diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart index c8d2e2b624..b2bf37b2bf 100644 --- a/mobile/lib/domain/utils/background_sync.dart +++ b/mobile/lib/domain/utils/background_sync.dart @@ -9,8 +9,57 @@ class BackgroundSyncManager { Cancelable? _deviceAlbumSyncTask; Cancelable? _hashTask; + Completer? _localSyncMutex; + Completer? _remoteSyncMutex; + Completer? _hashMutex; + BackgroundSyncManager(); + Future _withMutex( + Completer? Function() getMutex, + void Function(Completer?) setMutex, + Future Function() operation, + ) async { + while (getMutex() != null) { + await getMutex()!.future; + } + + final mutex = Completer(); + setMutex(mutex); + + try { + final result = await operation(); + return result; + } finally { + setMutex(null); + mutex.complete(); + } + } + + Future _withLocalSyncMutex(Future Function() operation) { + return _withMutex( + () => _localSyncMutex, + (mutex) => _localSyncMutex = mutex, + operation, + ); + } + + Future _withRemoteSyncMutex(Future Function() operation) { + return _withMutex( + () => _remoteSyncMutex, + (mutex) => _remoteSyncMutex = mutex, + operation, + ); + } + + Future _withHashMutex(Future Function() operation) { + return _withMutex( + () => _hashMutex, + (mutex) => _hashMutex = mutex, + operation, + ); + } + Future cancel() { final futures = []; @@ -25,51 +74,57 @@ class BackgroundSyncManager { // No need to cancel the task, as it can also be run when the user logs out Future syncLocal({bool full = false}) { - if (_deviceAlbumSyncTask != null) { - return _deviceAlbumSyncTask!.future; - } + return _withLocalSyncMutex(() async { + if (_deviceAlbumSyncTask != null) { + return _deviceAlbumSyncTask!.future; + } - // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being - // captured by the closure passed to [runInIsolateGentle]. - _deviceAlbumSyncTask = full - ? runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: true), - ) - : runInIsolateGentle( - computation: (ref) => - ref.read(localSyncServiceProvider).sync(full: false), - ); + // We use a ternary operator to avoid [_deviceAlbumSyncTask] from being + // captured by the closure passed to [runInIsolateGentle]. + _deviceAlbumSyncTask = full + ? runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: true), + ) + : runInIsolateGentle( + computation: (ref) => + ref.read(localSyncServiceProvider).sync(full: false), + ); - return _deviceAlbumSyncTask!.whenComplete(() { - _deviceAlbumSyncTask = null; + return _deviceAlbumSyncTask!.whenComplete(() { + _deviceAlbumSyncTask = null; + }); }); } // No need to cancel the task, as it can also be run when the user logs out Future hashAssets() { - if (_hashTask != null) { - return _hashTask!.future; - } + return _withHashMutex(() async { + if (_hashTask != null) { + return _hashTask!.future; + } - _hashTask = runInIsolateGentle( - computation: (ref) => ref.read(hashServiceProvider).hashAssets(), - ); - return _hashTask!.whenComplete(() { - _hashTask = null; + _hashTask = runInIsolateGentle( + computation: (ref) => ref.read(hashServiceProvider).hashAssets(), + ); + return _hashTask!.whenComplete(() { + _hashTask = null; + }); }); } Future syncRemote() { - if (_syncTask != null) { - return _syncTask!.future; - } + return _withRemoteSyncMutex(() async { + if (_syncTask != null) { + return _syncTask!.future; + } - _syncTask = runInIsolateGentle( - computation: (ref) => ref.read(syncStreamServiceProvider).sync(), - ); - return _syncTask!.whenComplete(() { - _syncTask = null; + _syncTask = runInIsolateGentle( + computation: (ref) => ref.read(syncStreamServiceProvider).sync(), + ); + return _syncTask!.whenComplete(() { + _syncTask = null; + }); }); } } diff --git a/mobile/lib/widgets/common/immich_sliver_app_bar.dart b/mobile/lib/widgets/common/immich_sliver_app_bar.dart index ff0e88e5d7..bff1ab4bb1 100644 --- a/mobile/lib/widgets/common/immich_sliver_app_bar.dart +++ b/mobile/lib/widgets/common/immich_sliver_app_bar.dart @@ -73,7 +73,10 @@ class ImmichSliverAppBar extends ConsumerWidget { onPressed: () => context.pop(), ), IconButton( - onPressed: () => ref.read(backgroundSyncProvider).syncRemote(), + onPressed: () { + ref.read(backgroundSyncProvider).syncLocal(full: true); + ref.read(backgroundSyncProvider).syncRemote(); + }, icon: const Icon( Icons.sync, ),