From 48ba7556b269757f07f38eaee224c690d427c393 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sat, 20 Jan 2024 12:43:35 +0100 Subject: [PATCH] fix: Transactions on web by doing them in the same way as on io This refactors the transaction workaround with "zone transactions" and abstracts them in a mixin. Then it just uses this mixin in the HiveDatabase and the sqflite_box and also applies them on indexedDB to fix transactions on web. --- lib/src/database/hive_database.dart | 55 ++-------------- lib/src/database/indexeddb_box.dart | 45 +++++++------ lib/src/database/sqflite_box.dart | 66 +++----------------- lib/src/database/zone_transaction_mixin.dart | 55 ++++++++++++++++ 4 files changed, 94 insertions(+), 127 deletions(-) create mode 100644 lib/src/database/zone_transaction_mixin.dart diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index cdcd7f94..8aa3706e 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -28,6 +28,7 @@ import 'package:matrix/encryption/utils/outbound_group_session.dart'; import 'package:matrix/encryption/utils/ssss_cache.dart'; import 'package:matrix/encryption/utils/stored_inbound_group_session.dart'; import 'package:matrix/matrix.dart'; +import 'package:matrix/src/database/zone_transaction_mixin.dart'; import 'package:matrix/src/utils/copy_map.dart'; import 'package:matrix/src/utils/queued_to_device_event.dart'; import 'package:matrix/src/utils/run_benchmarked.dart'; @@ -39,7 +40,7 @@ import 'package:matrix/src/utils/run_benchmarked.dart'; /// This database does not support file caching! @Deprecated( 'Use [HiveCollectionsDatabase] instead. Don\'t forget to properly migrate!') -class FamedlySdkHiveDatabase extends DatabaseApi { +class FamedlySdkHiveDatabase extends DatabaseApi with ZoneTransactionMixin { static const int version = 5; final String name; late Box _clientBox; @@ -1305,57 +1306,9 @@ class FamedlySdkHiveDatabase extends DatabaseApi { return; } - Completer? _transactionLock; - final _transactionZones = {}; - @override - Future transaction(Future Function() action) async { - // we want transactions to lock, however NOT if transactoins are run inside of each other. - // to be able to do this, we use dart zones (https://dart.dev/articles/archive/zones). - // _transactionZones holds a set of all zones which are currently running a transaction. - // _transactionLock holds the lock. - - // first we try to determine if we are inside of a transaction currently - var isInTransaction = false; - Zone? zone = Zone.current; - // for that we keep on iterating to the parent zone until there is either no zone anymore - // or we have found a zone inside of _transactionZones. - while (zone != null) { - if (_transactionZones.contains(zone)) { - isInTransaction = true; - break; - } - zone = zone.parent; - } - // if we are inside a transaction....just run the action - if (isInTransaction) { - return await action(); - } - // if we are *not* in a transaction, time to wait for the lock! - while (_transactionLock != null) { - await _transactionLock!.future; - } - // claim the lock - final lock = Completer(); - _transactionLock = lock; - try { - // run the action inside of a new zone - return await runZoned(() async { - try { - // don't forget to add the new zone to _transactionZones! - _transactionZones.add(Zone.current); - return await action(); - } finally { - // aaaand remove the zone from _transactionZones again - _transactionZones.remove(Zone.current); - } - }); - } finally { - // aaaand finally release the lock - _transactionLock = null; - lock.complete(); - } - } + Future transaction(Future Function() action) => + zoneTransaction(action); @override Future updateClient( diff --git a/lib/src/database/indexeddb_box.dart b/lib/src/database/indexeddb_box.dart index f4c3ccbe..b0803b3d 100644 --- a/lib/src/database/indexeddb_box.dart +++ b/lib/src/database/indexeddb_box.dart @@ -2,9 +2,11 @@ import 'dart:async'; import 'dart:html'; import 'dart:indexed_db'; +import 'package:matrix/src/database/zone_transaction_mixin.dart'; + /// Key-Value store abstraction over IndexedDB so that the sdk database can use /// a single interface for all platforms. API is inspired by Hive. -class BoxCollection { +class BoxCollection with ZoneTransactionMixin { final Database _db; final Set boxNames; final String _name; @@ -43,25 +45,28 @@ class BoxCollection { Future Function() action, { List? boxNames, bool readOnly = false, - }) async { - boxNames ??= _db.objectStoreNames!.toList(); - _txnCache = []; - await action(); - final cache = List Function(Transaction txn)>.from(_txnCache!); - _txnCache = null; - if (cache.isEmpty) return; - final txn = _db.transaction(boxNames, readOnly ? 'readonly' : 'readwrite'); - for (final fun in cache) { - // The IDB methods return a Future in Dart but must not be awaited in - // order to have an actual transaction. They must only be performed and - // then the transaction object must call `txn.completed;` which then - // returns the actual future. - // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction - unawaited(fun(txn)); - } - await txn.completed; - return; - } + }) => + zoneTransaction(() async { + boxNames ??= _db.objectStoreNames!.toList(); + final txnCache = _txnCache = []; + await action(); + final cache = + List Function(Transaction txn)>.from(txnCache); + _txnCache = null; + if (cache.isEmpty) return; + final txn = + _db.transaction(boxNames, readOnly ? 'readonly' : 'readwrite'); + for (final fun in cache) { + // The IDB methods return a Future in Dart but must not be awaited in + // order to have an actual transaction. They must only be performed and + // then the transaction object must call `txn.completed;` which then + // returns the actual future. + // https://developer.mozilla.org/en-US/docs/Web/API/IDBTransaction + unawaited(fun(txn)); + } + await txn.completed; + return; + }); Future clear() async { final txn = _db.transaction(boxNames, 'readwrite'); diff --git a/lib/src/database/sqflite_box.dart b/lib/src/database/sqflite_box.dart index 35c9bcec..459be4a7 100644 --- a/lib/src/database/sqflite_box.dart +++ b/lib/src/database/sqflite_box.dart @@ -3,9 +3,11 @@ import 'dart:convert'; import 'package:sqflite_common/sqflite.dart'; +import 'package:matrix/src/database/zone_transaction_mixin.dart'; + /// Key-Value store abstraction over Sqflite so that the sdk database can use /// a single interface for all platforms. API is inspired by Hive. -class BoxCollection { +class BoxCollection with ZoneTransactionMixin { final Database _db; final Set boxNames; final DatabaseFactory? _factory; @@ -42,66 +44,18 @@ class BoxCollection { Batch? _activeBatch; - Completer? _transactionLock; - final _transactionZones = {}; - Future transaction( Future Function() action, { List? boxNames, bool readOnly = false, - }) async { - // we want transactions to lock, however NOT if transactoins are run inside of each other. - // to be able to do this, we use dart zones (https://dart.dev/articles/archive/zones). - // _transactionZones holds a set of all zones which are currently running a transaction. - // _transactionLock holds the lock. - - // first we try to determine if we are inside of a transaction currently - var isInTransaction = false; - Zone? zone = Zone.current; - // for that we keep on iterating to the parent zone until there is either no zone anymore - // or we have found a zone inside of _transactionZones. - while (zone != null) { - if (_transactionZones.contains(zone)) { - isInTransaction = true; - break; - } - zone = zone.parent; - } - // if we are inside a transaction....just run the action - if (isInTransaction) { - return await action(); - } - // if we are *not* in a transaction, time to wait for the lock! - while (_transactionLock != null) { - await _transactionLock!.future; - } - // claim the lock - final lock = Completer(); - _transactionLock = lock; - try { - // run the action inside of a new zone - return await runZoned(() async { - try { - // don't forget to add the new zone to _transactionZones! - _transactionZones.add(Zone.current); - - final batch = _db.batch(); - _activeBatch = batch; - await action(); - _activeBatch = null; - await batch.commit(noResult: true); - return; - } finally { - // aaaand remove the zone from _transactionZones again - _transactionZones.remove(Zone.current); - } + }) => + zoneTransaction(() async { + final batch = _db.batch(); + _activeBatch = batch; + await action(); + _activeBatch = null; + await batch.commit(noResult: true); }); - } finally { - // aaaand finally release the lock - _transactionLock = null; - lock.complete(); - } - } Future clear() => transaction( () async { diff --git a/lib/src/database/zone_transaction_mixin.dart b/lib/src/database/zone_transaction_mixin.dart new file mode 100644 index 00000000..4c38f39d --- /dev/null +++ b/lib/src/database/zone_transaction_mixin.dart @@ -0,0 +1,55 @@ +import 'dart:async'; + +// we want transactions to lock, however NOT if transactoins are run inside of each other. +// to be able to do this, we use dart zones (https://dart.dev/articles/archive/zones). +// _transactionZones holds a set of all zones which are currently running a transaction. +// _transactionLock holds the lock. +mixin ZoneTransactionMixin { + Completer? _transactionLock; + final _transactionZones = {}; + + Future zoneTransaction(Future Function() action) async { + // first we try to determine if we are inside of a transaction currently + var isInTransaction = false; + Zone? zone = Zone.current; + // for that we keep on iterating to the parent zone until there is either no zone anymore + // or we have found a zone inside of _transactionZones. + while (zone != null) { + if (_transactionZones.contains(zone)) { + isInTransaction = true; + break; + } + zone = zone.parent; + } + // if we are inside a transaction....just run the action + if (isInTransaction) { + return await action(); + } + // if we are *not* in a transaction, time to wait for the lock! + while (_transactionLock != null) { + await _transactionLock!.future; + } + // claim the lock + final lock = Completer(); + _transactionLock = lock; + try { + // run the action inside of a new zone + return await runZoned(() async { + try { + // don't forget to add the new zone to _transactionZones! + _transactionZones.add(Zone.current); + + await action(); + return; + } finally { + // aaaand remove the zone from _transactionZones again + _transactionZones.remove(Zone.current); + } + }); + } finally { + // aaaand finally release the lock + _transactionLock = null; + lock.complete(); + } + } +}