mirror of
https://github.com/immich-app/immich.git
synced 2025-07-07 18:24:10 -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 {
|
|
||||||
|
SyncStreamService({
|
||||||
|
required ISyncApiRepository syncApiRepository,
|
||||||
|
required ISyncStreamRepository syncStreamRepository,
|
||||||
|
}) : _syncApiRepository = syncApiRepository,
|
||||||
|
_syncStreamRepository = syncStreamRepository;
|
||||||
|
|
||||||
|
// ignore: avoid-dynamic
|
||||||
|
Future<bool> _handleSyncData(dynamic data) async {
|
||||||
|
if (data is SyncPartnerV1) {
|
||||||
|
_logger.fine("SyncPartnerV1: $data");
|
||||||
|
return await _syncStreamRepository.updatePartnerV1(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data is SyncUserV1) {
|
||||||
|
_logger.fine("SyncUserV1: $data");
|
||||||
|
return await _syncStreamRepository.updateUsersV1(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data is SyncPartnerDeleteV1) {
|
||||||
|
_logger.fine("SyncPartnerDeleteV1: $data");
|
||||||
|
return await _syncStreamRepository.deletePartnerV1(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data is SyncUserDeleteV1) {
|
||||||
|
_logger.fine("SyncUserDeleteV1: $data");
|
||||||
|
return await _syncStreamRepository.deleteUsersV1(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _handleSyncEvents(List<SyncEvent> events) async {
|
||||||
|
Map<SyncEntityType, String> acks = {};
|
||||||
for (final event in events) {
|
for (final event in events) {
|
||||||
if (event.data is SyncUserV1) {
|
if (await _handleSyncData(event.data)) {
|
||||||
final data = event.data as SyncUserV1;
|
// Only retain the latest ack from each type
|
||||||
debugPrint("User Update: $data");
|
acks[event.type] = event.ack;
|
||||||
|
|
||||||
// final user = await _userRepository.get(data.id);
|
|
||||||
|
|
||||||
// if (user == null) {
|
|
||||||
// continue;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// user.name = data.name;
|
|
||||||
// user.email = data.email;
|
|
||||||
// user.updatedAt = DateTime.now();
|
|
||||||
|
|
||||||
// await _userRepository.update(user);
|
|
||||||
// await _syncApiRepository.ack(event.ack);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.data is SyncUserDeleteV1) {
|
|
||||||
final data = event.data as SyncUserDeleteV1;
|
|
||||||
|
|
||||||
debugPrint("User delete: $data");
|
|
||||||
// await _syncApiRepository.ack(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,28 +74,21 @@ 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const _kResponseMap = <SyncEntityType, Function(dynamic)>{
|
List<SyncEvent> _parseSyncResponse(List<String> lines) {
|
||||||
SyncEntityType.userV1: SyncUserV1.fromJson,
|
|
||||||
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Need to be outside of the class to be able to use compute
|
|
||||||
List<SyncEvent> _parseSyncResponse(List<String> lines) {
|
|
||||||
final List<SyncEvent> data = [];
|
final List<SyncEvent> data = [];
|
||||||
|
|
||||||
for (var line in lines) {
|
for (final line in lines) {
|
||||||
try {
|
try {
|
||||||
final jsonData = jsonDecode(line);
|
final jsonData = jsonDecode(line);
|
||||||
final type = SyncEntityType.fromJson(jsonData['type'])!;
|
final type = SyncEntityType.fromJson(jsonData['type'])!;
|
||||||
@ -98,15 +96,24 @@ List<SyncEvent> _parseSyncResponse(List<String> lines) {
|
|||||||
final ack = jsonData['ack'];
|
final ack = jsonData['ack'];
|
||||||
final converter = _kResponseMap[type];
|
final converter = _kResponseMap[type];
|
||||||
if (converter == null) {
|
if (converter == null) {
|
||||||
debugPrint("[_parseSyncReponse] Unknown type $type");
|
_logger.warning("[_parseSyncReponse] Unknown type $type");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
data.add(SyncEvent(data: converter(dataJson), ack: ack));
|
data.add(SyncEvent(type: type, data: converter(dataJson), ack: ack));
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
debugPrint("[_parseSyncReponse] Error parsing json $error $stack");
|
_logger.severe("[_parseSyncResponse] Error parsing json", error, stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ignore: avoid-dynamic
|
||||||
|
const _kResponseMap = <SyncEntityType, Function(dynamic)>{
|
||||||
|
SyncEntityType.userV1: SyncUserV1.fromJson,
|
||||||
|
SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson,
|
||||||
|
SyncEntityType.partnerV1: SyncPartnerV1.fromJson,
|
||||||
|
SyncEntityType.partnerDeleteV1: SyncPartnerDeleteV1.fromJson,
|
||||||
|
};
|
||||||
|
@ -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(
|
||||||
ref.watch(syncApiRepositoryProvider),
|
syncApiRepository: 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