diff --git a/lib/src/database/hive_database.dart b/lib/src/database/hive_database.dart index 507562da..0f2e181f 100644 --- a/lib/src/database/hive_database.dart +++ b/lib/src/database/hive_database.dart @@ -1049,8 +1049,57 @@ class FamedlySdkHiveDatabase extends DatabaseApi { return; } + Completer? _transactionLock; + final _transactionZones = {}; + @override - Future transaction(Future Function() action) => action(); + 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(); + } + } @override Future updateClient( diff --git a/test/database_api_test.dart b/test/database_api_test.dart index a370c0d6..2bb09986 100644 --- a/test/database_api_test.dart +++ b/test/database_api_test.dart @@ -18,6 +18,7 @@ */ import 'dart:convert'; import 'dart:typed_data'; +import 'dart:async'; import 'package:matrix/matrix.dart'; import 'package:test/test.dart'; @@ -49,6 +50,31 @@ void testDatabase(Future futureDatabase, int clientId) { test('Open', () async { database = await futureDatabase; }); + test('transaction', () async { + print('Starting test...'); + var counter = 0; + await database.transaction(() async { + expect(counter++, 0); + await database.transaction(() async { + expect(counter++, 1); + await Future.delayed(Duration(milliseconds: 50)); + expect(counter++, 2); + }); + expect(counter++, 3); + }); + + // we can't use Zone.root.run inside of tests so we abuse timers instead + Timer(Duration(milliseconds: 50), () async { + await database.transaction(() async { + expect(counter++, 6); + }); + }); + await database.transaction(() async { + expect(counter++, 4); + await Future.delayed(Duration(milliseconds: 100)); + expect(counter++, 5); + }); + }); test('insertIntoToDeviceQueue', () async { toDeviceQueueIndex = await database.insertIntoToDeviceQueue( clientId,