From 83c2bbffd2d83db19341be9ae7388fa4dd08aca5 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 10 Jun 2026 15:48:08 +0200 Subject: [PATCH 1/2] Support latest sqlite3_web package --- packages/sqlite_async/CHANGELOG.md | 3 ++- packages/sqlite_async/pubspec.yaml | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 367af19..5d18052 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,6 +1,7 @@ -## 0.14.3 (unreleased) +## 0.14.3 - Include identifier of mutexes when a navigator lock attempt is aborted. +- Support versions `0.9.x` of `package:sqlite3_web`. ## 0.14.2 diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 45dde39..2975811 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.14.2 +version: 0.14.3 resolution: workspace repository: https://github.com/powersync-ja/sqlite_async.dart environment: @@ -14,7 +14,7 @@ topics: dependencies: sqlite3: ^3.2.0 - sqlite3_web: ^0.8.0 + sqlite3_web: '>=0.8.0 <0.10.0' sqlite3_connection_pool: ^0.2.4 async: ^2.10.0 collection: ^1.17.0 From 858dccc05b274ead3c5273501db1c8e4badfa231 Mon Sep 17 00:00:00 2001 From: Simon Binder Date: Wed, 10 Jun 2026 15:46:25 +0200 Subject: [PATCH 2/2] Close databases when they become unreferenced --- packages/sqlite_async/CHANGELOG.md | 6 +- .../sqlite_async/lib/src/web/database.dart | 11 ++- .../src/web/database/async_web_database.dart | 29 +++++-- .../sqlite_async/lib/src/web/finalizer.dart | 77 +++++++++++++++++++ .../lib/src/web/web_sqlite_open_factory.dart | 23 ++---- packages/sqlite_async/pubspec.yaml | 2 +- 6 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 packages/sqlite_async/lib/src/web/finalizer.dart diff --git a/packages/sqlite_async/CHANGELOG.md b/packages/sqlite_async/CHANGELOG.md index 5d18052..4384944 100644 --- a/packages/sqlite_async/CHANGELOG.md +++ b/packages/sqlite_async/CHANGELOG.md @@ -1,7 +1,11 @@ +## 0.14.4 (unreleased) + +- Web: Stop leaking dedicated web workers when databases are closed. + ## 0.14.3 - Include identifier of mutexes when a navigator lock attempt is aborted. -- Support versions `0.9.x` of `package:sqlite3_web`. +- Web: Stop leaking dedicated web workers when databases are closed. ## 0.14.2 diff --git a/packages/sqlite_async/lib/src/web/database.dart b/packages/sqlite_async/lib/src/web/database.dart index 3e464f4..268222b 100644 --- a/packages/sqlite_async/lib/src/web/database.dart +++ b/packages/sqlite_async/lib/src/web/database.dart @@ -13,6 +13,7 @@ import '../common/sqlite_database.dart'; import '../common/timeouts.dart'; import '../impl/context.dart'; import 'connection.dart'; +import 'finalizer.dart'; import 'protocol.dart'; import 'web_mutex.dart'; @@ -29,6 +30,12 @@ final class WebDatabase extends SqliteDatabaseImpl /// web broadcast channels to forward local update events to other tabs. final BroadcastUpdates? broadcastUpdates; + /// The [WebSqlite] wrapper from which this database instance is derived. + /// + /// When all databases derived from the same [WebSqlite] instances are closed + /// or no longer referenced, this automatically invoked [WebSqlite.close]. + final FinalizableWebSqliteResource? _finalizableSource; + @override bool closed = false; @@ -39,11 +46,13 @@ final class WebDatabase extends SqliteDatabaseImpl required this.profileQueries, required this.updates, this.broadcastUpdates, - }); + FinalizableWebSqliteResource? finalizable, + }) : _finalizableSource = finalizable; @override Future close() async { await _database.dispose(); + _finalizableSource?.close(); closed = true; } diff --git a/packages/sqlite_async/lib/src/web/database/async_web_database.dart b/packages/sqlite_async/lib/src/web/database/async_web_database.dart index 6084c66..2f140d0 100644 --- a/packages/sqlite_async/lib/src/web/database/async_web_database.dart +++ b/packages/sqlite_async/lib/src/web/database/async_web_database.dart @@ -51,15 +51,27 @@ final class AsyncWebDatabaseImpl extends SqliteDatabaseImpl _connection = await openFactory.openConnection( SqliteOpenOptions(primaryConnection: true, readOnly: false)); - final broadcastUpdates = _connection.broadcastUpdates; + _broadcastUpdatesSubscription = + _installUpdatesListener(_connection, updatesController); + } + + // The updated might be coming from a single static stream controller, we want + // to avoid capturing `this` in a subscription. + static StreamSubscription? _installUpdatesListener( + WebDatabase connection, + StreamController updatesController) { + final broadcastUpdates = connection.broadcastUpdates; + final localUpdates = updatesController; + StreamSubscription? broadcastUpdatesSubscription; + if (broadcastUpdates == null) { // We can use updates directly from the database. - _connection.updates.forEach((update) { - updatesController.add(update); + connection.updates.forEach((update) { + localUpdates.add(update); }); } else { - _connection.updates.forEach((update) { - updatesController.add(update); + connection.updates.forEach((update) { + localUpdates.add(update); // Share local updates with other tabs broadcastUpdates.send(update); @@ -67,11 +79,12 @@ final class AsyncWebDatabaseImpl extends SqliteDatabaseImpl // Also add updates from other tabs, note that things we send aren't // received by our tab. - _broadcastUpdatesSubscription = - broadcastUpdates.updates.listen((updates) { - updatesController.add(updates); + broadcastUpdatesSubscription = broadcastUpdates.updates.listen((updates) { + localUpdates.add(updates); }); } + + return broadcastUpdatesSubscription; } T _runZoned(T Function() callback, {required String debugContext}) { diff --git a/packages/sqlite_async/lib/src/web/finalizer.dart b/packages/sqlite_async/lib/src/web/finalizer.dart new file mode 100644 index 0000000..7dbf3d9 --- /dev/null +++ b/packages/sqlite_async/lib/src/web/finalizer.dart @@ -0,0 +1,77 @@ +import 'package:sqlite3_web/sqlite3_web.dart'; + +import '../sqlite_options.dart'; + +/// A ref-counted [WebSqlite] wrapper. +/// +/// Each reference is represented as [FinalizableWebSqliteResource], which have +/// a finalizer attached to them. When all resources pointing to this instance +/// are finalized, we can close the inner [WebSqlite] instance. +final class _RefCountedWebSqlite { + final Future instance; + int _users = 0; + + _RefCountedWebSqlite._(this.instance); + + static (_RefCountedWebSqlite, FinalizableWebSqliteResource) create( + Future instance) { + final ref = _RefCountedWebSqlite._(instance); + ref._users = 1; + return (ref, FinalizableWebSqliteResource._(ref)); + } + + FinalizableWebSqliteResource reference() { + assert(_users > 0); + _users++; + return FinalizableWebSqliteResource._(this); + } + + void decrementUsers() { + _users--; + if (_users == 0) { + instance.then((sqlite) => sqlite.close()); + } + } +} + +final class FinalizableWebSqliteResource { + final _RefCountedWebSqlite _instance; + var _closed = false; + + FinalizableWebSqliteResource._(this._instance) { + _finalizer.attach(this, _instance, detach: this); + } + + Future get sqlite => _instance.instance; + + void close() { + if (!_closed) { + _closed = true; + _instance.decrementUsers(); + _finalizer.detach(this); + } + } + + FinalizableWebSqliteResource clone() => + FinalizableWebSqliteResource._(_instance); + + static final Finalizer<_RefCountedWebSqlite> _finalizer = + Finalizer((r) => r.decrementUsers()); +} + +/// Active [WebSqlite] instance, keyed by options. +final Map _activeSqliteInstances = {}; + +FinalizableWebSqliteResource resolveWebSqliteResource( + WebSqliteOptions options, Future Function() open) { + final cacheKey = options.wasmUri + options.workerUri; + + if (_activeSqliteInstances[cacheKey] case final instance? + when instance._users > 0) { + return instance.reference(); + } + + final (sqlite, ref) = _RefCountedWebSqlite.create(open()); + _activeSqliteInstances[cacheKey] = sqlite; + return ref; +} diff --git a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart index bcd6592..dad844a 100644 --- a/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart +++ b/packages/sqlite_async/lib/src/web/web_sqlite_open_factory.dart @@ -8,33 +8,25 @@ import 'package:sqlite_async/src/web/web_mutex.dart'; import '../common/abstract_open_factory.dart'; import 'database.dart'; +import 'finalizer.dart'; import 'update_notifications.dart'; import 'worker/worker_utils.dart'; final UpdateNotificationStreams _updateStreams = UpdateNotificationStreams(); -Map> _webSQLiteImplementations = {}; /// [SqliteOpenFactory] implementation for the web. /// /// This class can be extended to customize how databases are opened on the web. base class WebSqliteOpenFactory extends InternalOpenFactory { - late final Future _initialized = Future.sync(() { - final cacheKey = sqliteOptions.webSqliteOptions.wasmUri + - sqliteOptions.webSqliteOptions.workerUri; - - if (_webSQLiteImplementations.containsKey(cacheKey)) { - return _webSQLiteImplementations[cacheKey]!; - } - - _webSQLiteImplementations[cacheKey] = - openWebSqlite(sqliteOptions.webSqliteOptions); - return _webSQLiteImplementations[cacheKey]!; - }); + late final _openedWebSqlite = resolveWebSqliteResource( + sqliteOptions.webSqliteOptions, + () => openWebSqlite(sqliteOptions.webSqliteOptions), + ); WebSqliteOpenFactory( {required super.path, super.sqliteOptions = const SqliteOptions()}) { // Make sure initializer starts running immediately - _initialized; + _openedWebSqlite; } /// Opens a [WebSqlite] instance for the given [options]. @@ -73,7 +65,7 @@ base class WebSqliteOpenFactory extends InternalOpenFactory { /// Due to being asynchronous, the under laying CommonDatabase is not /// accessible Future openConnection(SqliteOpenOptions options) async { - final workers = await _initialized; + final workers = await _openedWebSqlite.sqlite; final connection = await connectToWorker(workers, path); final pragmaStatements = this.pragmaStatements(options); @@ -116,6 +108,7 @@ base class WebSqliteOpenFactory extends InternalOpenFactory { broadcastUpdates: broadcastUpdates, profileQueries: sqliteOptions.profileQueries, updates: updatesFor(connection.database), + finalizable: _openedWebSqlite.clone(), ); } diff --git a/packages/sqlite_async/pubspec.yaml b/packages/sqlite_async/pubspec.yaml index 2975811..3d126d2 100644 --- a/packages/sqlite_async/pubspec.yaml +++ b/packages/sqlite_async/pubspec.yaml @@ -14,7 +14,7 @@ topics: dependencies: sqlite3: ^3.2.0 - sqlite3_web: '>=0.8.0 <0.10.0' + sqlite3_web: ^0.9.0 sqlite3_connection_pool: ^0.2.4 async: ^2.10.0 collection: ^1.17.0