mirror of
https://github.com/immich-app/immich.git
synced 2026-05-30 19:35:19 -04:00
15f8afbd81
wire hash cancellation await cleanup remove forced kill add regression tests abort sync requests fix cleanup ordering in teardown exit isolate test background sync test sigabrt crash cleanup
253 lines
7.4 KiB
Dart
253 lines
7.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:immich_mobile/domain/utils/migrate_cloud_ids.dart' as m;
|
|
import 'package:immich_mobile/domain/utils/sync_linked_album.dart';
|
|
import 'package:immich_mobile/providers/infrastructure/sync.provider.dart';
|
|
import 'package:immich_mobile/utils/isolate.dart';
|
|
import 'package:worker_manager/worker_manager.dart';
|
|
|
|
typedef SyncCallback = void Function();
|
|
typedef SyncCallbackWithResult<T> = void Function(T result);
|
|
typedef SyncErrorCallback = void Function(String error);
|
|
|
|
class BackgroundSyncManager {
|
|
final SyncCallback? onRemoteSyncStart;
|
|
final SyncCallbackWithResult<bool?>? onRemoteSyncComplete;
|
|
final SyncErrorCallback? onRemoteSyncError;
|
|
|
|
final SyncCallback? onLocalSyncStart;
|
|
final SyncCallback? onLocalSyncComplete;
|
|
final SyncErrorCallback? onLocalSyncError;
|
|
|
|
final SyncCallback? onHashingStart;
|
|
final SyncCallback? onHashingComplete;
|
|
final SyncErrorCallback? onHashingError;
|
|
|
|
final SyncCallback? onCloudIdSyncStart;
|
|
final SyncCallback? onCloudIdSyncComplete;
|
|
final SyncErrorCallback? onCloudIdSyncError;
|
|
|
|
Cancelable<bool?>? _syncTask;
|
|
Cancelable<void>? _syncWebsocketTask;
|
|
Cancelable<void>? _cloudIdSyncTask;
|
|
Cancelable<void>? _deviceAlbumSyncTask;
|
|
Cancelable<void>? _linkedAlbumSyncTask;
|
|
Cancelable<void>? _hashTask;
|
|
|
|
BackgroundSyncManager({
|
|
this.onRemoteSyncStart,
|
|
this.onRemoteSyncComplete,
|
|
this.onRemoteSyncError,
|
|
this.onLocalSyncStart,
|
|
this.onLocalSyncComplete,
|
|
this.onLocalSyncError,
|
|
this.onHashingStart,
|
|
this.onHashingComplete,
|
|
this.onHashingError,
|
|
this.onCloudIdSyncStart,
|
|
this.onCloudIdSyncComplete,
|
|
this.onCloudIdSyncError,
|
|
});
|
|
|
|
Future<void> cancel() async {
|
|
final tasks = [
|
|
_syncTask,
|
|
_syncWebsocketTask,
|
|
_cloudIdSyncTask,
|
|
_linkedAlbumSyncTask,
|
|
_deviceAlbumSyncTask,
|
|
_hashTask,
|
|
];
|
|
final futures = [
|
|
for (final task in tasks)
|
|
if (task != null) task.future,
|
|
];
|
|
for (final task in tasks) {
|
|
task?.cancel();
|
|
}
|
|
_syncTask = null;
|
|
_syncWebsocketTask = null;
|
|
_cloudIdSyncTask = null;
|
|
_linkedAlbumSyncTask = null;
|
|
_deviceAlbumSyncTask = null;
|
|
_hashTask = null;
|
|
|
|
try {
|
|
await Future.wait(futures);
|
|
} on CanceledError {
|
|
// Ignore cancellation errors
|
|
}
|
|
}
|
|
|
|
// No need to cancel the task, as it can also be run when the user logs out
|
|
Future<void> syncLocal({bool full = false}) {
|
|
if (_deviceAlbumSyncTask != null) {
|
|
return _deviceAlbumSyncTask!.future;
|
|
}
|
|
|
|
onLocalSyncStart?.call();
|
|
|
|
// 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),
|
|
debugLabel: 'local-sync-full-true',
|
|
)
|
|
: runInIsolateGentle(
|
|
computation: (ref) => ref.read(localSyncServiceProvider).sync(full: false),
|
|
debugLabel: 'local-sync-full-false',
|
|
);
|
|
|
|
return _deviceAlbumSyncTask!
|
|
.whenComplete(() {
|
|
_deviceAlbumSyncTask = null;
|
|
onLocalSyncComplete?.call();
|
|
})
|
|
.catchError((error) {
|
|
onLocalSyncError?.call(error.toString());
|
|
_deviceAlbumSyncTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> hashAssets() {
|
|
if (_hashTask != null) {
|
|
return _hashTask!.future;
|
|
}
|
|
|
|
onHashingStart?.call();
|
|
|
|
_hashTask = runInIsolateGentle(
|
|
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
|
debugLabel: 'hash-assets',
|
|
);
|
|
|
|
return _hashTask!
|
|
.whenComplete(() {
|
|
onHashingComplete?.call();
|
|
_hashTask = null;
|
|
})
|
|
.catchError((error) {
|
|
onHashingError?.call(error.toString());
|
|
_hashTask = null;
|
|
});
|
|
}
|
|
|
|
Future<bool> syncRemote() {
|
|
if (_syncTask != null) {
|
|
return _syncTask!.future.then((result) => result ?? false).catchError((_) => false);
|
|
}
|
|
|
|
onRemoteSyncStart?.call();
|
|
|
|
_syncTask = runInIsolateGentle(
|
|
computation: (ref) => ref.read(syncStreamServiceProvider).sync(),
|
|
debugLabel: 'remote-sync',
|
|
);
|
|
return _syncTask!
|
|
.then((result) {
|
|
final success = result ?? false;
|
|
onRemoteSyncComplete?.call(success);
|
|
return success;
|
|
})
|
|
.catchError((error) {
|
|
onRemoteSyncError?.call(error.toString());
|
|
_syncTask = null;
|
|
return false;
|
|
})
|
|
.whenComplete(() {
|
|
_syncTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncWebsocketBatchV1(List<dynamic> batchData) {
|
|
if (_syncWebsocketTask != null) {
|
|
return _syncWebsocketTask!.future;
|
|
}
|
|
_syncWebsocketTask = _handleWsAssetUploadReadyV1Batch(batchData);
|
|
return _syncWebsocketTask!.whenComplete(() {
|
|
_syncWebsocketTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncWebsocketBatchV2(List<dynamic> batchData) {
|
|
if (_syncWebsocketTask != null) {
|
|
return _syncWebsocketTask!.future;
|
|
}
|
|
_syncWebsocketTask = _handleWsAssetUploadReadyV2Batch(batchData);
|
|
return _syncWebsocketTask!.whenComplete(() {
|
|
_syncWebsocketTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncWebsocketEditV1(dynamic data) {
|
|
if (_syncWebsocketTask != null) {
|
|
return _syncWebsocketTask!.future;
|
|
}
|
|
_syncWebsocketTask = _handleWsAssetEditReadyV1(data);
|
|
return _syncWebsocketTask!.whenComplete(() {
|
|
_syncWebsocketTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncWebsocketEditV2(dynamic data) {
|
|
if (_syncWebsocketTask != null) {
|
|
return _syncWebsocketTask!.future;
|
|
}
|
|
_syncWebsocketTask = _handleWsAssetEditReadyV2(data);
|
|
return _syncWebsocketTask!.whenComplete(() {
|
|
_syncWebsocketTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncLinkedAlbum() {
|
|
if (_linkedAlbumSyncTask != null) {
|
|
return _linkedAlbumSyncTask!.future;
|
|
}
|
|
|
|
_linkedAlbumSyncTask = runInIsolateGentle(computation: syncLinkedAlbumsIsolated, debugLabel: 'linked-album-sync');
|
|
return _linkedAlbumSyncTask!.whenComplete(() {
|
|
_linkedAlbumSyncTask = null;
|
|
});
|
|
}
|
|
|
|
Future<void> syncCloudIds() {
|
|
if (_cloudIdSyncTask != null) {
|
|
return _cloudIdSyncTask!.future;
|
|
}
|
|
|
|
onCloudIdSyncStart?.call();
|
|
|
|
_cloudIdSyncTask = runInIsolateGentle(computation: m.syncCloudIds);
|
|
return _cloudIdSyncTask!
|
|
.whenComplete(() {
|
|
onCloudIdSyncComplete?.call();
|
|
_cloudIdSyncTask = null;
|
|
})
|
|
.catchError((error) {
|
|
onCloudIdSyncError?.call(error.toString());
|
|
_cloudIdSyncTask = null;
|
|
});
|
|
}
|
|
}
|
|
|
|
Cancelable<void> _handleWsAssetUploadReadyV1Batch(List<dynamic> batchData) => runInIsolateGentle(
|
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV1Batch(batchData),
|
|
debugLabel: 'websocket-batch',
|
|
);
|
|
|
|
Cancelable<void> _handleWsAssetUploadReadyV2Batch(List<dynamic> batchData) => runInIsolateGentle(
|
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV2Batch(batchData),
|
|
debugLabel: 'websocket-batch',
|
|
);
|
|
|
|
Cancelable<void> _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
|
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
|
|
debugLabel: 'websocket-edit',
|
|
);
|
|
|
|
Cancelable<void> _handleWsAssetEditReadyV2(dynamic data) => runInIsolateGentle(
|
|
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV2(data),
|
|
debugLabel: 'websocket-edit',
|
|
);
|