mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 10:37:11 -04:00 
			
		
		
		
	feat: user & partner sync stream
This commit is contained in:
		
							parent
							
								
									734566db50
								
							
						
					
					
						commit
						3739e86974
					
				| @ -92,6 +92,8 @@ custom_lint: | |||||||
|       allowed: |       allowed: | ||||||
|         # required / wanted |         # required / wanted | ||||||
|         - lib/repositories/*_api.repository.dart |         - lib/repositories/*_api.repository.dart | ||||||
|  |         - lib/domain/models/sync/sync_event.model.dart | ||||||
|  |         - lib/{domain,infrastructure}/**/sync_stream.* | ||||||
|         - lib/infrastructure/repositories/*_api.repository.dart |         - lib/infrastructure/repositories/*_api.repository.dart | ||||||
|         - lib/infrastructure/utils/*.converter.dart |         - lib/infrastructure/utils/*.converter.dart | ||||||
|         # acceptable exceptions for the time being |         # acceptable exceptions for the time being | ||||||
|  | |||||||
| @ -5,5 +5,7 @@ const double downloadFailed = -2; | |||||||
| // Number of log entries to retain on app start | // Number of log entries to retain on app start | ||||||
| const int kLogTruncateLimit = 250; | const int kLogTruncateLimit = 250; | ||||||
| 
 | 
 | ||||||
|  | // Duration for backgroun sync | ||||||
|  | const Duration kBackgroundSyncDuration = Duration(minutes: 1); | ||||||
| const int kBatchHashFileLimit = 128; | const int kBatchHashFileLimit = 128; | ||||||
| const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB | const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB | ||||||
|  | |||||||
| @ -1,7 +1,9 @@ | |||||||
| import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; | import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; | ||||||
| 
 | 
 | ||||||
| abstract interface class ISyncApiRepository { | abstract interface class ISyncApiRepository { | ||||||
|   Future<void> ack(String data); |   Future<void> ack(List<String> data); | ||||||
| 
 | 
 | ||||||
|   Stream<List<SyncEvent>> watchUserSyncEvent(); |   Stream<List<SyncEvent>> watchUserSyncEvent(); | ||||||
|  | 
 | ||||||
|  |   Stream<List<SyncEvent>> watchPartnerSyncEvent(); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								mobile/lib/domain/interfaces/sync_stream.interface.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								mobile/lib/domain/interfaces/sync_stream.interface.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | import 'package:immich_mobile/domain/interfaces/db.interface.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  | 
 | ||||||
|  | abstract interface class ISyncStreamRepository implements IDatabaseRepository { | ||||||
|  |   Future<bool> updateUsersV1(SyncUserV1 data); | ||||||
|  |   Future<bool> deleteUsersV1(SyncUserDeleteV1 data); | ||||||
|  | 
 | ||||||
|  |   Future<bool> updatePartnerV1(SyncPartnerV1 data); | ||||||
|  |   Future<bool> deletePartnerV1(SyncPartnerDeleteV1 data); | ||||||
|  | } | ||||||
| @ -1,14 +1,13 @@ | |||||||
| class SyncEvent { | import 'package:openapi/api.dart'; | ||||||
|   // dynamic |  | ||||||
|   final dynamic data; |  | ||||||
| 
 | 
 | ||||||
|  | class SyncEvent { | ||||||
|  |   final SyncEntityType type; | ||||||
|  |   // ignore: avoid-dynamic | ||||||
|  |   final dynamic data; | ||||||
|   final String ack; |   final String ack; | ||||||
| 
 | 
 | ||||||
|   SyncEvent({ |   const SyncEvent({required this.type, required this.data, required this.ack}); | ||||||
|     required this.data, |  | ||||||
|     required this.ack, |  | ||||||
|   }); |  | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'SyncEvent(data: $data, ack: $ack)'; |   String toString() => 'SyncEvent(type: $type, data: $data, ack: $ack)'; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,49 +1,96 @@ | |||||||
|  | // ignore_for_file: avoid-passing-async-when-sync-expected | ||||||
|  | 
 | ||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| 
 | 
 | ||||||
| import 'package:flutter/foundation.dart'; |  | ||||||
| import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; | import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; | ||||||
|  | import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; | ||||||
|  | import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| 
 | 
 | ||||||
| class SyncStreamService { | class SyncStreamService { | ||||||
|   final ISyncApiRepository _syncApiRepository; |   final Logger _logger = Logger('SyncStreamService'); | ||||||
| 
 | 
 | ||||||
|   SyncStreamService(this._syncApiRepository); |   final ISyncApiRepository _syncApiRepository; | ||||||
|  |   final ISyncStreamRepository _syncStreamRepository; | ||||||
| 
 | 
 | ||||||
|   StreamSubscription? _userSyncSubscription; |   StreamSubscription? _userSyncSubscription; | ||||||
|  |   Completer<void> _userSyncCompleter = Completer<void>(); | ||||||
| 
 | 
 | ||||||
|   void syncUsers() { |   StreamSubscription? _partnerSyncSubscription; | ||||||
|     _userSyncSubscription = |   Completer<void> _partnerSyncCompleter = Completer<void>(); | ||||||
|         _syncApiRepository.watchUserSyncEvent().listen((events) async { |  | ||||||
|       for (final event in events) { |  | ||||||
|         if (event.data is SyncUserV1) { |  | ||||||
|           final data = event.data as SyncUserV1; |  | ||||||
|           debugPrint("User Update: $data"); |  | ||||||
| 
 | 
 | ||||||
|           // final user = await _userRepository.get(data.id); |   SyncStreamService({ | ||||||
|  |     required ISyncApiRepository syncApiRepository, | ||||||
|  |     required ISyncStreamRepository syncStreamRepository, | ||||||
|  |   })  : _syncApiRepository = syncApiRepository, | ||||||
|  |         _syncStreamRepository = syncStreamRepository; | ||||||
| 
 | 
 | ||||||
|           // if (user == null) { |   // ignore: avoid-dynamic | ||||||
|           //   continue; |   Future<bool> _handleSyncData(dynamic data) async { | ||||||
|           // } |     if (data is SyncPartnerV1) { | ||||||
|  |       _logger.fine("SyncPartnerV1: $data"); | ||||||
|  |       return await _syncStreamRepository.updatePartnerV1(data); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|           // user.name = data.name; |     if (data is SyncUserV1) { | ||||||
|           // user.email = data.email; |       _logger.fine("SyncUserV1: $data"); | ||||||
|           // user.updatedAt = DateTime.now(); |       return await _syncStreamRepository.updateUsersV1(data); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|           // await _userRepository.update(user); |     if (data is SyncPartnerDeleteV1) { | ||||||
|           // await _syncApiRepository.ack(event.ack); |       _logger.fine("SyncPartnerDeleteV1: $data"); | ||||||
|         } |       return await _syncStreamRepository.deletePartnerV1(data); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|         if (event.data is SyncUserDeleteV1) { |     if (data is SyncUserDeleteV1) { | ||||||
|           final data = event.data as SyncUserDeleteV1; |       _logger.fine("SyncUserDeleteV1: $data"); | ||||||
|  |       return await _syncStreamRepository.deleteUsersV1(data); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|           debugPrint("User delete: $data"); |     return false; | ||||||
|           // await _syncApiRepository.ack(event.ack); |   } | ||||||
|         } | 
 | ||||||
|  |   Future<void> _handleSyncEvents(List<SyncEvent> events) async { | ||||||
|  |     Map<SyncEntityType, String> acks = {}; | ||||||
|  |     for (final event in events) { | ||||||
|  |       if (await _handleSyncData(event.data)) { | ||||||
|  |         // Only retain the latest ack from each type | ||||||
|  |         acks[event.type] = event.ack; | ||||||
|       } |       } | ||||||
|     }); |     } | ||||||
|  |     await _syncApiRepository.ack(acks.values.toList()); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> syncUsers() async { | ||||||
|  |     _logger.info("Syncing User Changes"); | ||||||
|  |     _userSyncSubscription = _syncApiRepository.watchUserSyncEvent().listen( | ||||||
|  |       _handleSyncEvents, | ||||||
|  |       onDone: () { | ||||||
|  |         _userSyncCompleter.complete(); | ||||||
|  |         _userSyncCompleter = Completer<void>(); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |     return await _userSyncCompleter.future; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> syncPartners() async { | ||||||
|  |     _logger.info("Syncing Partner Changes"); | ||||||
|  |     _partnerSyncSubscription = | ||||||
|  |         _syncApiRepository.watchPartnerSyncEvent().listen( | ||||||
|  |       _handleSyncEvents, | ||||||
|  |       onDone: () { | ||||||
|  |         _partnerSyncCompleter.complete(); | ||||||
|  |         _partnerSyncCompleter = Completer<void>(); | ||||||
|  |       }, | ||||||
|  |     ); | ||||||
|  |     return await _partnerSyncCompleter.future; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Future<void> dispose() async { |   Future<void> dispose() async { | ||||||
|     await _userSyncSubscription?.cancel(); |     await _userSyncSubscription?.cancel(); | ||||||
|  |     _userSyncCompleter.complete(); | ||||||
|  |     await _partnerSyncSubscription?.cancel(); | ||||||
|  |     _partnerSyncCompleter.complete(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,27 +1,31 @@ | |||||||
| import 'dart:async'; | import 'dart:async'; | ||||||
| import 'dart:convert'; | import 'dart:convert'; | ||||||
| 
 | 
 | ||||||
| import 'package:flutter/foundation.dart'; | import 'package:http/http.dart' as http; | ||||||
| import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; | import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; | ||||||
| import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; | import 'package:immich_mobile/domain/models/sync/sync_event.model.dart'; | ||||||
| import 'package:immich_mobile/services/api.service.dart'; | import 'package:immich_mobile/services/api.service.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
| import 'package:openapi/api.dart'; | import 'package:openapi/api.dart'; | ||||||
| import 'package:http/http.dart' as http; |  | ||||||
| 
 | 
 | ||||||
| class SyncApiRepository implements ISyncApiRepository { | class SyncApiRepository implements ISyncApiRepository { | ||||||
|  |   final Logger _logger = Logger('SyncApiRepository'); | ||||||
|   final ApiService _api; |   final ApiService _api; | ||||||
|   const SyncApiRepository(this._api); |   SyncApiRepository(this._api); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Stream<List<SyncEvent>> watchUserSyncEvent() { |   Stream<List<SyncEvent>> watchUserSyncEvent() { | ||||||
|     return _getSyncStream( |     return _getSyncStream(SyncStreamDto(types: [SyncRequestType.usersV1])); | ||||||
|       SyncStreamDto(types: [SyncRequestType.usersV1]), |  | ||||||
|     ); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   Future<void> ack(String data) { |   Stream<List<SyncEvent>> watchPartnerSyncEvent() { | ||||||
|     return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: [data])); |     return _getSyncStream(SyncStreamDto(types: [SyncRequestType.partnersV1])); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<void> ack(List<String> data) { | ||||||
|  |     return _api.syncApi.sendSyncAck(SyncAckSetDto(acks: data)); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   Stream<List<SyncEvent>> _getSyncStream( |   Stream<List<SyncEvent>> _getSyncStream( | ||||||
| @ -31,7 +35,7 @@ class SyncApiRepository implements ISyncApiRepository { | |||||||
|     final client = http.Client(); |     final client = http.Client(); | ||||||
|     final endpoint = "${_api.apiClient.basePath}/sync/stream"; |     final endpoint = "${_api.apiClient.basePath}/sync/stream"; | ||||||
| 
 | 
 | ||||||
|     final headers = <String, String>{ |     final headers = { | ||||||
|       'Content-Type': 'application/json', |       'Content-Type': 'application/json', | ||||||
|       'Accept': 'application/jsonlines+json', |       'Accept': 'application/jsonlines+json', | ||||||
|     }; |     }; | ||||||
| @ -61,7 +65,8 @@ class SyncApiRepository implements ISyncApiRepository { | |||||||
| 
 | 
 | ||||||
|       await for (final chunk in response.stream.transform(utf8.decoder)) { |       await for (final chunk in response.stream.transform(utf8.decoder)) { | ||||||
|         previousChunk += chunk; |         previousChunk += chunk; | ||||||
|         final parts = previousChunk.split('\n'); |         // ignore: move-variable-outside-iteration | ||||||
|  |         final parts = previousChunk.toString().split('\n'); | ||||||
|         previousChunk = parts.removeLast(); |         previousChunk = parts.removeLast(); | ||||||
|         lines.addAll(parts); |         lines.addAll(parts); | ||||||
| 
 | 
 | ||||||
| @ -69,44 +74,46 @@ class SyncApiRepository implements ISyncApiRepository { | |||||||
|           continue; |           continue; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         yield await compute(_parseSyncResponse, lines); |         yield _parseSyncResponse(lines); | ||||||
|         lines.clear(); |         lines.clear(); | ||||||
|       } |       } | ||||||
|     } finally { |     } finally { | ||||||
|       if (lines.isNotEmpty) { |       if (lines.isNotEmpty) { | ||||||
|         yield await compute(_parseSyncResponse, lines); |         yield _parseSyncResponse(lines); | ||||||
|       } |       } | ||||||
|       client.close(); |       client.close(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   List<SyncEvent> _parseSyncResponse(List<String> lines) { | ||||||
|  |     final List<SyncEvent> data = []; | ||||||
|  | 
 | ||||||
|  |     for (final line in lines) { | ||||||
|  |       try { | ||||||
|  |         final jsonData = jsonDecode(line); | ||||||
|  |         final type = SyncEntityType.fromJson(jsonData['type'])!; | ||||||
|  |         final dataJson = jsonData['data']; | ||||||
|  |         final ack = jsonData['ack']; | ||||||
|  |         final converter = _kResponseMap[type]; | ||||||
|  |         if (converter == null) { | ||||||
|  |           _logger.warning("[_parseSyncReponse] Unknown type $type"); | ||||||
|  |           continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack)); | ||||||
|  |       } catch (error, stack) { | ||||||
|  |         _logger.severe("[_parseSyncResponse] Error parsing json", error, stack); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return data; | ||||||
|  |   } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // ignore: avoid-dynamic | ||||||
| const _kResponseMap = <SyncEntityType, Function(dynamic)>{ | const _kResponseMap = <SyncEntityType, Function(dynamic)>{ | ||||||
|   SyncEntityType.userV1: SyncUserV1.fromJson, |   SyncEntityType.userV1: SyncUserV1.fromJson, | ||||||
|   SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, |   SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, | ||||||
|  |   SyncEntityType.partnerV1: SyncPartnerV1.fromJson, | ||||||
|  |   SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson, | ||||||
| }; | }; | ||||||
| 
 |  | ||||||
| // Need to be outside of the class to be able to use compute |  | ||||||
| List<SyncEvent> _parseSyncResponse(List<String> lines) { |  | ||||||
|   final List<SyncEvent> data = []; |  | ||||||
| 
 |  | ||||||
|   for (var line in lines) { |  | ||||||
|     try { |  | ||||||
|       final jsonData = jsonDecode(line); |  | ||||||
|       final type = SyncEntityType.fromJson(jsonData['type'])!; |  | ||||||
|       final dataJson = jsonData['data']; |  | ||||||
|       final ack = jsonData['ack']; |  | ||||||
|       final converter = _kResponseMap[type]; |  | ||||||
|       if (converter == null) { |  | ||||||
|         debugPrint("[_parseSyncReponse] Unknown type $type"); |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
| 
 |  | ||||||
|       data.add(SyncEvent(data: converter(dataJson), ack: ack)); |  | ||||||
|     } catch (error, stack) { |  | ||||||
|       debugPrint("[_parseSyncReponse] Error parsing json $error $stack"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   return data; |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -0,0 +1,84 @@ | |||||||
|  | import 'package:drift/drift.dart'; | ||||||
|  | import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  | 
 | ||||||
|  | class DriftSyncStreamRepository extends DriftDatabaseRepository | ||||||
|  |     implements ISyncStreamRepository { | ||||||
|  |   final Logger _logger = Logger('DriftSyncStreamRepository'); | ||||||
|  |   final Drift _db; | ||||||
|  | 
 | ||||||
|  |   DriftSyncStreamRepository(super.db) : _db = db; | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> deleteUsersV1(SyncUserDeleteV1 data) async { | ||||||
|  |     try { | ||||||
|  |       await _db.managers.userEntity | ||||||
|  |           .filter((row) => row.id.equals(data.userId)) | ||||||
|  |           .delete(); | ||||||
|  |       return true; | ||||||
|  |     } catch (e, s) { | ||||||
|  |       _logger.severe('Error while processing SyncUserDeleteV1', e, s); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> updateUsersV1(SyncUserV1 data) async { | ||||||
|  |     final companion = UserEntityCompanion( | ||||||
|  |       name: Value(data.name), | ||||||
|  |       email: Value(data.email), | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       await _db.userEntity.insertOne( | ||||||
|  |         companion.copyWith(id: Value(data.id)), | ||||||
|  |         onConflict: DoUpdate((_) => companion), | ||||||
|  |       ); | ||||||
|  |       return true; | ||||||
|  |     } catch (e, s) { | ||||||
|  |       _logger.severe('Error while processing SyncUserV1', e, s); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> deletePartnerV1(SyncPartnerDeleteV1 data) async { | ||||||
|  |     try { | ||||||
|  |       await _db.managers.partnerEntity | ||||||
|  |           .filter( | ||||||
|  |             (row) => | ||||||
|  |                 row.sharedById.id.equals(data.sharedById) & | ||||||
|  |                 row.sharedWithId.id.equals(data.sharedWithId), | ||||||
|  |           ) | ||||||
|  |           .delete(); | ||||||
|  |       return true; | ||||||
|  |     } catch (e, s) { | ||||||
|  |       _logger.severe('Error while processing SyncPartnerDeleteV1', e, s); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   Future<bool> updatePartnerV1(SyncPartnerV1 data) async { | ||||||
|  |     final companion = | ||||||
|  |         PartnerEntityCompanion(inTimeline: Value(data.inTimeline)); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       await _db.partnerEntity.insertOne( | ||||||
|  |         companion.copyWith( | ||||||
|  |           sharedById: Value(data.sharedById), | ||||||
|  |           sharedWithId: Value(data.sharedWithId), | ||||||
|  |         ), | ||||||
|  |         onConflict: DoUpdate((_) => companion), | ||||||
|  |       ); | ||||||
|  |       return true; | ||||||
|  |     } catch (e, s) { | ||||||
|  |       _logger.severe('Error while processing SyncUserV1', e, s); | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -7,6 +7,7 @@ import 'package:immich_mobile/models/backup/backup_state.model.dart'; | |||||||
| import 'package:immich_mobile/providers/album/album.provider.dart'; | import 'package:immich_mobile/providers/album/album.provider.dart'; | ||||||
| import 'package:immich_mobile/providers/asset.provider.dart'; | import 'package:immich_mobile/providers/asset.provider.dart'; | ||||||
| import 'package:immich_mobile/providers/auth.provider.dart'; | import 'package:immich_mobile/providers/auth.provider.dart'; | ||||||
|  | import 'package:immich_mobile/providers/background_sync.provider.dart'; | ||||||
| import 'package:immich_mobile/providers/backup/backup.provider.dart'; | import 'package:immich_mobile/providers/backup/backup.provider.dart'; | ||||||
| import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; | import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart'; | ||||||
| import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; | import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; | ||||||
| @ -113,6 +114,7 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> { | |||||||
|         _ref.read(backupProvider.notifier).cancelBackup(); |         _ref.read(backupProvider.notifier).cancelBackup(); | ||||||
|       } |       } | ||||||
|       _ref.read(websocketProvider.notifier).disconnect(); |       _ref.read(websocketProvider.notifier).disconnect(); | ||||||
|  |       _ref.read(backgroundSyncProvider).stop(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     LogService.I.flush(); |     LogService.I.flush(); | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								mobile/lib/providers/background_sync.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								mobile/lib/providers/background_sync.provider.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/constants/constants.dart'; | ||||||
|  | import 'package:immich_mobile/utils/background_sync.dart'; | ||||||
|  | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
|  | 
 | ||||||
|  | part 'background_sync.provider.g.dart'; | ||||||
|  | 
 | ||||||
|  | @Riverpod(keepAlive: true) | ||||||
|  | BackgroundSyncManager backgroundSync(Ref _) => | ||||||
|  |     BackgroundSyncManager(duration: kBackgroundSyncDuration); | ||||||
							
								
								
									
										27
									
								
								mobile/lib/providers/background_sync.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								mobile/lib/providers/background_sync.provider.g.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | // GENERATED CODE - DO NOT MODIFY BY HAND | ||||||
|  | 
 | ||||||
|  | part of 'background_sync.provider.dart'; | ||||||
|  | 
 | ||||||
|  | // ************************************************************************** | ||||||
|  | // RiverpodGenerator | ||||||
|  | // ************************************************************************** | ||||||
|  | 
 | ||||||
|  | String _$backgroundSyncHash() => r'c08b6499af18d3fedeb9bb6c4ac0833c656f30dd'; | ||||||
|  | 
 | ||||||
|  | /// See also [backgroundSync]. | ||||||
|  | @ProviderFor(backgroundSync) | ||||||
|  | final backgroundSyncProvider = Provider<BackgroundSyncManager>.internal( | ||||||
|  |   backgroundSync, | ||||||
|  |   name: r'backgroundSyncProvider', | ||||||
|  |   debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') | ||||||
|  |       ? null | ||||||
|  |       : _$backgroundSyncHash, | ||||||
|  |   dependencies: null, | ||||||
|  |   allTransitiveDependencies: null, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef BackgroundSyncRef = ProviderRef<BackgroundSyncManager>; | ||||||
|  | // ignore_for_file: type=lint | ||||||
|  | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
| @ -1,5 +1,10 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; | ||||||
| import 'package:isar/isar.dart'; | import 'package:isar/isar.dart'; | ||||||
|  | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| 
 | 
 | ||||||
| // overwritten in main.dart due to async loading | // overwritten in main.dart due to async loading | ||||||
| final dbProvider = Provider<Isar>((_) => throw UnimplementedError()); | final dbProvider = Provider<Isar>((_) => throw UnimplementedError()); | ||||||
|  | 
 | ||||||
|  | @Riverpod(keepAlive: true) | ||||||
|  | Drift drift(Ref _) => Drift(); | ||||||
|  | |||||||
| @ -1,4 +1,5 @@ | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; | ||||||
| import 'package:isar/isar.dart'; | import 'package:isar/isar.dart'; | ||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||
| 
 | 
 | ||||||
| @ -6,3 +7,6 @@ part 'db.provider.g.dart'; | |||||||
| 
 | 
 | ||||||
| @Riverpod(keepAlive: true) | @Riverpod(keepAlive: true) | ||||||
| Isar isar(Ref ref) => throw UnimplementedError('isar'); | Isar isar(Ref ref) => throw UnimplementedError('isar'); | ||||||
|  | 
 | ||||||
|  | @Riverpod(keepAlive: true) | ||||||
|  | Drift drift(Ref _) => Drift(); | ||||||
|  | |||||||
| @ -22,5 +22,21 @@ final isarProvider = Provider<Isar>.internal( | |||||||
| @Deprecated('Will be removed in 3.0. Use Ref instead') | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
| // ignore: unused_element | // ignore: unused_element | ||||||
| typedef IsarRef = ProviderRef<Isar>; | typedef IsarRef = ProviderRef<Isar>; | ||||||
|  | String _$driftHash() => r'21ceb1dbc569b877cb710387b350590e994fe64e'; | ||||||
|  | 
 | ||||||
|  | /// See also [drift]. | ||||||
|  | @ProviderFor(drift) | ||||||
|  | final driftProvider = Provider<Drift>.internal( | ||||||
|  |   drift, | ||||||
|  |   name: r'driftProvider', | ||||||
|  |   debugGetCreateSourceHash: | ||||||
|  |       const bool.fromEnvironment('dart.vm.product') ? null : _$driftHash, | ||||||
|  |   dependencies: null, | ||||||
|  |   allTransitiveDependencies: null, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | @Deprecated('Will be removed in 3.0. Use Ref instead') | ||||||
|  | // ignore: unused_element | ||||||
|  | typedef DriftRef = ProviderRef<Drift>; | ||||||
| // ignore_for_file: type=lint | // ignore_for_file: type=lint | ||||||
| // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package | ||||||
|  | |||||||
| @ -3,22 +3,25 @@ import 'dart:async'; | |||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| import 'package:immich_mobile/domain/services/sync_stream.service.dart'; | import 'package:immich_mobile/domain/services/sync_stream.service.dart'; | ||||||
| import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; | import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; | ||||||
|  | import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; | ||||||
| import 'package:immich_mobile/providers/api.provider.dart'; | import 'package:immich_mobile/providers/api.provider.dart'; | ||||||
|  | import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||||
| 
 | 
 | ||||||
| final syncStreamServiceProvider = Provider( | final syncStreamServiceProvider = Provider((ref) { | ||||||
|   (ref) { |   final instance = SyncStreamService( | ||||||
|     final instance = SyncStreamService( |     syncApiRepository: ref.watch(syncApiRepositoryProvider), | ||||||
|       ref.watch(syncApiRepositoryProvider), |     syncStreamRepository: ref.watch(syncStreamRepositoryProvider), | ||||||
|     ); |   ); | ||||||
| 
 | 
 | ||||||
|     ref.onDispose(() => unawaited(instance.dispose())); |   ref.onDispose(() => unawaited(instance.dispose())); | ||||||
| 
 | 
 | ||||||
|     return instance; |   return instance; | ||||||
|   }, | }); | ||||||
| ); |  | ||||||
| 
 | 
 | ||||||
| final syncApiRepositoryProvider = Provider( | final syncApiRepositoryProvider = Provider( | ||||||
|   (ref) => SyncApiRepository( |   (ref) => SyncApiRepository(ref.watch(apiServiceProvider)), | ||||||
|     ref.watch(apiServiceProvider), | ); | ||||||
|   ), | 
 | ||||||
|  | final syncStreamRepositoryProvider = Provider( | ||||||
|  |   (ref) => DriftSyncStreamRepository(ref.watch(driftProvider)), | ||||||
| ); | ); | ||||||
|  | |||||||
							
								
								
									
										58
									
								
								mobile/lib/utils/background_sync.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								mobile/lib/utils/background_sync.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,58 @@ | |||||||
|  | // ignore_for_file: avoid-passing-async-when-sync-expected | ||||||
|  | 
 | ||||||
|  | import 'dart:async'; | ||||||
|  | 
 | ||||||
|  | import 'package:async/async.dart'; | ||||||
|  | import 'package:immich_mobile/providers/infrastructure/sync_stream.provider.dart'; | ||||||
|  | import 'package:immich_mobile/utils/isolate.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
|  | 
 | ||||||
|  | class _SyncStreamDriver { | ||||||
|  |   final _userSyncCache = AsyncCache.ephemeral(); | ||||||
|  |   final _partnerSyncCache = AsyncCache.ephemeral(); | ||||||
|  | 
 | ||||||
|  |   Future<void> syncUsers() => _userSyncCache.fetch( | ||||||
|  |         () async => runInIsolate( | ||||||
|  |           (ref) => ref.read(syncStreamServiceProvider).syncUsers(), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  | 
 | ||||||
|  |   Future<void> syncPartners() => _partnerSyncCache.fetch( | ||||||
|  |         () async => runInIsolate( | ||||||
|  |           (ref) => ref.read(syncStreamServiceProvider).syncPartners(), | ||||||
|  |         ), | ||||||
|  |       ); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | class BackgroundSyncManager { | ||||||
|  |   final Logger _logger = Logger('BackgroundSyncManager'); | ||||||
|  |   Timer? _timer; | ||||||
|  |   final Duration _duration; | ||||||
|  |   // This allows us to keep synching in the background while allowing ondemand syncs | ||||||
|  |   final _driver = _SyncStreamDriver(); | ||||||
|  | 
 | ||||||
|  |   BackgroundSyncManager({required Duration duration}) : _duration = duration; | ||||||
|  | 
 | ||||||
|  |   Timer _createTimer() { | ||||||
|  |     return Timer.periodic(_duration, (timer) async { | ||||||
|  |       _logger.info('Background sync started'); | ||||||
|  |       await _driver.syncUsers(); | ||||||
|  |       await _driver.syncPartners(); | ||||||
|  |       _logger.info('Background sync completed'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void start() { | ||||||
|  |     _logger.info('Background sync enabled'); | ||||||
|  |     _timer ??= _createTimer(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   void stop() { | ||||||
|  |     _logger.info('Background sync disabled'); | ||||||
|  |     _timer?.cancel(); | ||||||
|  |     _timer = null; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   Future<void> syncUsers() => _driver.syncUsers(); | ||||||
|  |   Future<void> syncPartners() => _driver.syncPartners(); | ||||||
|  | } | ||||||
							
								
								
									
										67
									
								
								mobile/lib/utils/isolate.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								mobile/lib/utils/isolate.dart
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  | import 'dart:isolate'; | ||||||
|  | import 'dart:ui'; | ||||||
|  | 
 | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter/services.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/providers/db.provider.dart'; | ||||||
|  | import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; | ||||||
|  | import 'package:immich_mobile/utils/bootstrap.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
|  | 
 | ||||||
|  | class InvalidIsolateUsageException implements Exception { | ||||||
|  |   const InvalidIsolateUsageException(); | ||||||
|  | 
 | ||||||
|  |   @override | ||||||
|  |   String toString() => | ||||||
|  |       "IsolateHelper should only be used from the root isolate"; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // !! Should be used only from the root isolate | ||||||
|  | Future<T?> runInIsolate<T>( | ||||||
|  |   FutureOr<T> Function(ProviderContainer ref) computation, { | ||||||
|  |   String? debugLabel, | ||||||
|  | }) async { | ||||||
|  |   final token = RootIsolateToken.instance; | ||||||
|  |   if (token == null) { | ||||||
|  |     throw const InvalidIsolateUsageException(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return await Isolate.run(() async { | ||||||
|  |     BackgroundIsolateBinaryMessenger.ensureInitialized(token); | ||||||
|  |     DartPluginRegistrant.ensureInitialized(); | ||||||
|  | 
 | ||||||
|  |     final db = await Bootstrap.initIsar(); | ||||||
|  |     await Bootstrap.initDomain(db); | ||||||
|  |     final ref = ProviderContainer( | ||||||
|  |       overrides: [ | ||||||
|  |         // TODO: Remove once isar is removed | ||||||
|  |         dbProvider.overrideWithValue(db), | ||||||
|  |         isarProvider.overrideWithValue(db), | ||||||
|  |       ], | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     Logger log = Logger("IsolateLogger"); | ||||||
|  | 
 | ||||||
|  |     try { | ||||||
|  |       final result = await computation(ref); | ||||||
|  |       // Wait for isolate to end; i.e, logs to be flushed | ||||||
|  |       await Future.delayed(Durations.short2); | ||||||
|  |       return result; | ||||||
|  |     } catch (error, stack) { | ||||||
|  |       // Log the error and stack trace | ||||||
|  |       log.severe( | ||||||
|  |         "Error in runInIsolate${debugLabel == null ? '' : ' for $debugLabel'}", | ||||||
|  |         error, | ||||||
|  |         stack, | ||||||
|  |       ); | ||||||
|  |     } finally { | ||||||
|  |       // Always close the new database connection on Isolate end | ||||||
|  |       ref.read(driftProvider).close(); | ||||||
|  |       ref.read(dbProvider).close(); | ||||||
|  |       ref.read(isarProvider).close(); | ||||||
|  |     } | ||||||
|  |     return null; | ||||||
|  |   }); | ||||||
|  | } | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user