From 79a33a78194604c2c54594ea3233997d3f8d5da6 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Tue, 3 Sep 2024 22:11:23 +0200 Subject: [PATCH 1/5] Add methods for exchanging database connections --- .../sqlite_async/lib/src/web/database.dart | 8 ++++- packages/sqlite_async/lib/web.dart | 35 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 packages/sqlite_async/lib/web.dart diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index b632aa7..64f4351 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -5,11 +5,12 @@ import 'package:sqlite3/common.dart'; import 'package:sqlite3_web/sqlite3_web.dart'; import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; +import 'package:sqlite_async/web.dart'; import 'protocol.dart'; class WebDatabase with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase { + implements SqliteDatabase, WebSqliteConnection { final Database _database; final Mutex? _mutex; @@ -56,6 +57,11 @@ class WebDatabase /// Not relevant for web. Never get openFactory => throw UnimplementedError(); + @override + Future exposeEndpoint() async { + return await _database.additionalConnection(); + } + @override Future readLock(Future Function(SqliteReadContext tx) callback, {Duration? lockTimeout, String? debugContext}) async { diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart new file mode 100644 index 0000000..4ee430b --- /dev/null +++ b/packages/sqlite_async/lib/web.dart @@ -0,0 +1,35 @@ +/// +library sqlite_async.web; + +import 'package:sqlite3_web/sqlite3_web.dart'; +import 'sqlite_async.dart'; +import 'src/web/database.dart'; + +/// A [SqliteConnection] interface implemented by opened connections when +/// running on the web. +/// +/// This adds the [exposeEndpoint], which uses `dart:js_interop` types not +/// supported on native Dart platforms. The method can be used to access an +/// opened database across different JavaScript contexts +/// (e.g. document windows and workers). +abstract class WebSqliteConnection implements SqliteConnection { + /// Returns a [SqliteWebEndpoint] from `package:sqlite3/web.dart` - a + /// structure that consists only of types that can be transferred across a + /// `MessagePort` in JavaScript. + /// + /// After transferring this endpoint to another JavaScript context (e.g. a + /// worker), the worker can call [connectToEndpoint] to obtain a connection to + /// the same sqlite database. + Future exposeEndpoint(); + + /// Connect to an endpoint obtained through [exposeEndpoint]. + /// + /// The endpoint is transferrable in JavaScript, allowing multiple JavaScript + /// contexts to exchange opened database connections. + static Future connectToEndpoint( + SqliteWebEndpoint endpoint) async { + final rawSqlite = await WebSqlite.connectToPort(endpoint); + final database = WebDatabase(rawSqlite, null); + return database; + } +} From 77039d081a3963c39bd80f0c7784de22c713e3ab Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 18 Sep 2024 17:56:37 +0200 Subject: [PATCH 2/5] Expose lock name as well --- .../sqlite_async/lib/src/web/database.dart | 14 ++++++- .../sqlite_async/lib/src/web/web_mutex.dart | 6 +-- packages/sqlite_async/lib/web.dart | 37 +++++++++++++++---- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 64f4351..8a3ce0e 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -7,6 +7,7 @@ import 'package:sqlite_async/sqlite_async.dart'; import 'package:sqlite_async/src/utils/shared_utils.dart'; import 'package:sqlite_async/web.dart'; import 'protocol.dart'; +import 'web_mutex.dart'; class WebDatabase with SqliteQueries, SqliteDatabaseMixin @@ -58,8 +59,17 @@ class WebDatabase Never get openFactory => throw UnimplementedError(); @override - Future exposeEndpoint() async { - return await _database.additionalConnection(); + Future exposeEndpoint() async { + final endpoint = await _database.additionalConnection(); + + return ( + connectPort: endpoint.$1, + connectName: endpoint.$2, + lockName: switch (_mutex) { + MutexImpl(:final resolvedIdentifier) => resolvedIdentifier, + _ => null, + }, + ); } @override diff --git a/packages/sqlite_async/lib/src/web/web_mutex.dart b/packages/sqlite_async/lib/src/web/web_mutex.dart index 4013201..38f7ff5 100644 --- a/packages/sqlite_async/lib/src/web/web_mutex.dart +++ b/packages/sqlite_async/lib/src/web/web_mutex.dart @@ -18,7 +18,7 @@ external Navigator get _navigator; class MutexImpl implements Mutex { late final mutex.Mutex fallback; String? identifier; - final String _resolvedIdentifier; + final String resolvedIdentifier; MutexImpl({this.identifier}) @@ -29,7 +29,7 @@ class MutexImpl implements Mutex { /// - The uuid package could be added for better uniqueness if required. /// This would add another package dependency to `sqlite_async` which is potentially unnecessary at this point. /// An identifier should be supplied for better exclusion. - : _resolvedIdentifier = identifier ?? + : resolvedIdentifier = identifier ?? "${DateTime.now().microsecondsSinceEpoch}-${Random().nextDouble()}" { fallback = mutex.Mutex(); } @@ -125,7 +125,7 @@ class MutexImpl implements Mutex { final lockOptions = JSObject(); lockOptions['signal'] = controller.signal; final promise = _navigator.locks - .request(_resolvedIdentifier, lockOptions, jsCallback.toJS); + .request(resolvedIdentifier, lockOptions, jsCallback.toJS); // A timeout abort will throw an exception which needs to be handled. // There should not be any other unhandled lock errors. js_util.promiseToFuture(promise).catchError((error) {}); diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart index 4ee430b..987a7c0 100644 --- a/packages/sqlite_async/lib/web.dart +++ b/packages/sqlite_async/lib/web.dart @@ -2,9 +2,25 @@ library sqlite_async.web; import 'package:sqlite3_web/sqlite3_web.dart'; +import 'package:web/web.dart'; import 'sqlite_async.dart'; import 'src/web/database.dart'; +/// An endpoint that can be used, by any running JavaScript context in the same +/// website, to connect to an existing [WebSqliteConnection]. +/// +/// These endpoints are created by calling [WebSqliteConnection.exposeEndpoint] +/// and consist of a [MessagePort] and two [String]s internally identifying the +/// connection. Both objects can be transferred over send ports towards another +/// worker or context. That context can then use +/// [WebSqliteConnection.connectToEndpoint] to connect to the port already +/// opened. +typedef WebDatabaseEndpoint = ({ + MessagePort connectPort, + String connectName, + String? lockName, +}); + /// A [SqliteConnection] interface implemented by opened connections when /// running on the web. /// @@ -13,23 +29,30 @@ import 'src/web/database.dart'; /// opened database across different JavaScript contexts /// (e.g. document windows and workers). abstract class WebSqliteConnection implements SqliteConnection { - /// Returns a [SqliteWebEndpoint] from `package:sqlite3/web.dart` - a - /// structure that consists only of types that can be transferred across a - /// `MessagePort` in JavaScript. + /// Returns a [WebDatabaseEndpoint] - a structure that consists only of types + /// that can be transferred across a [MessagePort] in JavaScript. /// /// After transferring this endpoint to another JavaScript context (e.g. a /// worker), the worker can call [connectToEndpoint] to obtain a connection to /// the same sqlite database. - Future exposeEndpoint(); + Future exposeEndpoint(); /// Connect to an endpoint obtained through [exposeEndpoint]. /// /// The endpoint is transferrable in JavaScript, allowing multiple JavaScript /// contexts to exchange opened database connections. static Future connectToEndpoint( - SqliteWebEndpoint endpoint) async { - final rawSqlite = await WebSqlite.connectToPort(endpoint); - final database = WebDatabase(rawSqlite, null); + WebDatabaseEndpoint endpoint) async { + final rawSqlite = await WebSqlite.connectToPort( + (endpoint.connectPort, endpoint.connectName)); + + final database = WebDatabase( + rawSqlite, + switch (endpoint.lockName) { + var lock? => Mutex(identifier: lock), + null => null, + }, + ); return database; } } From e371281f2e2cbf2820f731cfce021b305cb90561 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 18 Sep 2024 21:50:20 +0200 Subject: [PATCH 3/5] Also implement interface in sqlite database impl --- .../src/web/database/web_sqlite_database.dart | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 522b48e..05dd6bb 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -10,12 +10,15 @@ import 'package:sqlite_async/src/sqlite_options.dart'; import 'package:sqlite_async/src/update_notification.dart'; import 'package:sqlite_async/src/web/web_mutex.dart'; import 'package:sqlite_async/src/web/web_sqlite_open_factory.dart'; +import 'package:sqlite_async/web.dart'; + +import '../database.dart'; /// Web implementation of [SqliteDatabase] /// Uses a web worker for SQLite connection class SqliteDatabaseImpl with SqliteQueries, SqliteDatabaseMixin - implements SqliteDatabase { + implements SqliteDatabase, WebSqliteConnection { @override bool get closed { return _connection.closed; @@ -38,7 +41,7 @@ class SqliteDatabaseImpl AbstractDefaultSqliteOpenFactory openFactory; late final Mutex mutex; - late final SqliteConnection _connection; + late final WebDatabase _connection; /// Open a SqliteDatabase. /// @@ -78,8 +81,8 @@ class SqliteDatabaseImpl Future _init() async { _connection = await openFactory.openConnection(SqliteOpenOptions( - primaryConnection: true, readOnly: false, mutex: mutex)); - _connection.updates!.forEach((update) { + primaryConnection: true, readOnly: false, mutex: mutex)) as WebDatabase; + _connection.updates.forEach((update) { updatesController.add(update); }); } @@ -139,4 +142,9 @@ class SqliteDatabaseImpl await isInitialized; return _connection.getAutoCommit(); } + + @override + Future exposeEndpoint() async { + return await _connection.exposeEndpoint(); + } } From aab14de5d36df7ce4aa600516de3b45745846bc5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 18 Sep 2024 21:55:11 +0200 Subject: [PATCH 4/5] Add changelog entry --- packages/sqlite_async/CHANGELOG.md | 6 ++++++ packages/sqlite_async/lib/web.dart | 3 +++ packages/sqlite_async/pubspec.yaml | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index a91538e..9f546d4 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,3 +1,9 @@ +## 0.10.0 + +- Add the `exposeEndpoint()` method available on web databases. It returns a serializable + description of the database endpoint that can be sent across workers. + This allows sharing an opened database connection across workers. + ## 0.9.0 - Support the latest version of package:web and package:sqlite3_web diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart index 987a7c0..4e202b3 100644 --- a/packages/sqlite_async/lib/web.dart +++ b/packages/sqlite_async/lib/web.dart @@ -1,4 +1,7 @@ +/// Exposes interfaces implemented by database implementations on the web. /// +/// These expose methods allowing database instances to be shared across web +/// workers. library sqlite_async.web; import 'package:sqlite3_web/sqlite3_web.dart'; diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 438acbb..983e736 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -1,6 +1,6 @@ name: sqlite_async description: High-performance asynchronous interface for SQLite on Dart and Flutter. -version: 0.9.0 +version: 0.10.0-dev repository: https://github.com/powersync-ja/sqlite_async.dart environment: sdk: ">=3.4.0 <4.0.0" From aedd45b08b19e7f237f54a70c6ea31f5939d311e Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Sun, 27 Oct 2024 18:43:34 +0100 Subject: [PATCH 5/5] Web: Return when database is closed --- packages/sqlite_async/lib/src/web/database.dart | 3 +++ .../lib/src/web/database/web_sqlite_database.dart | 3 +++ packages/sqlite_async/lib/web.dart | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 8a3ce0e..96026d2 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -26,6 +26,9 @@ class WebDatabase closed = true; } + @override + Future get closedFuture => _database.closed; + @override Future getAutoCommit() async { final response = await _database.customRequest( diff --git a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart index 05dd6bb..c7e2a70 100644 --- a/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart +++ b/packages/sqlite_async/lib/src/web/database/web_sqlite_database.dart @@ -24,6 +24,9 @@ class SqliteDatabaseImpl return _connection.closed; } + @override + Future get closedFuture => _connection.closedFuture; + final StreamController updatesController = StreamController.broadcast(); diff --git a/packages/sqlite_async/lib/web.dart b/packages/sqlite_async/lib/web.dart index 4e202b3..7c49737 100644 --- a/packages/sqlite_async/lib/web.dart +++ b/packages/sqlite_async/lib/web.dart @@ -32,6 +32,13 @@ typedef WebDatabaseEndpoint = ({ /// opened database across different JavaScript contexts /// (e.g. document windows and workers). abstract class WebSqliteConnection implements SqliteConnection { + /// Returns a future that completes when this connection is closed. + /// + /// This usually only happens when calling [close], but on the web + /// specifically, it can also happen when a remote context closes a database + /// accessed via [connectToEndpoint]. + Future get closedFuture; + /// Returns a [WebDatabaseEndpoint] - a structure that consists only of types /// that can be transferred across a [MessagePort] in JavaScript. ///