Fix startup sync results not propagated to main isolate

This commit is contained in:
Ming Ming 2024-01-18 01:17:53 +08:00
parent a04ecefed9
commit f97476e4f0
13 changed files with 229 additions and 45 deletions

View file

@ -13,6 +13,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/progress_util.dart';
import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/rx_extension.dart';
import 'package:nc_photos/use_case/file/list_file.dart'; import 'package:nc_photos/use_case/file/list_file.dart';
import 'package:nc_photos/use_case/find_file_descriptor.dart';
import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/remove.dart';
import 'package:nc_photos/use_case/sync_dir.dart'; import 'package:nc_photos/use_case/sync_dir.dart';
import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/use_case/update_property.dart';
@ -20,6 +21,7 @@ import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/lazy.dart'; import 'package:np_common/lazy.dart';
import 'package:np_common/object_util.dart'; import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart'; import 'package:np_common/or_null.dart';
import 'package:np_db/np_db.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:to_string/to_string.dart'; import 'package:to_string/to_string.dart';
@ -233,6 +235,69 @@ class FilesController {
} }
} }
Future<void> applySyncResult({
DbSyncIdResult? favorites,
List<int>? fileExifs,
}) async {
if (favorites?.isNotEmpty != true && fileExifs?.isNotEmpty != true) {
return;
}
// do async ops first
final fileExifFiles =
await fileExifs?.letFuture((e) async => await FindFileDescriptor(_c)(
account,
e,
onFileNotFound: (id) {
_log.warning("[applySyncResult] File id not found: $id");
},
));
final next = LinkedHashMap.of(_dataStreamController.value.files);
if (favorites != null && favorites.isNotEmpty) {
_applySyncFavoriteResult(next, favorites);
}
if (fileExifFiles != null && fileExifFiles.isNotEmpty) {
_applySyncFileExifResult(next, fileExifFiles);
}
_dataStreamController.addWithValue((value) => value.copyWith(files: next));
}
void _applySyncFavoriteResult(
Map<int, FileDescriptor> next, DbSyncIdResult result) {
for (final id in result.insert) {
final f = next[id];
if (f == null) {
_log.warning("[_applySyncFavoriteResult] File id not found: $id");
continue;
}
if (f is File) {
next[id] = f.copyWith(isFavorite: true);
} else {
next[id] = f.copyWith(fdIsFavorite: true);
}
}
for (final id in result.delete) {
final f = next[id];
if (f == null) {
_log.warning("[_applySyncFavoriteResult] File id not found: $id");
continue;
}
if (f is File) {
next[id] = f.copyWith(isFavorite: false);
} else {
next[id] = f.copyWith(fdIsFavorite: false);
}
}
}
void _applySyncFileExifResult(
Map<int, FileDescriptor> next, List<FileDescriptor> results) {
for (final f in results) {
next[f.fdId] = f;
}
}
Future<void> _load() async { Future<void> _load() async {
var lastData = _FilesStreamEvent( var lastData = _FilesStreamEvent(
files: const {}, files: const {},

View file

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/use_case/startup_sync.dart'; import 'package:nc_photos/use_case/startup_sync.dart';
@ -15,14 +17,19 @@ class SyncController {
_isDisposed = true; _isDisposed = true;
} }
Future<void> requestSync( Future<void> requestSync({
Account account, PersonProvider personProvider) async { required Account account,
required FilesController filesController,
required PersonsController personsController,
required PersonProvider personProvider,
}) async {
if (_isDisposed) { if (_isDisposed) {
return; return;
} }
if (_syncCompleter == null) { if (_syncCompleter == null) {
_syncCompleter = Completer(); _syncCompleter = Completer();
final result = await StartupSync.runInIsolate(account, personProvider); final result = await StartupSync.runInIsolate(
account, filesController, personsController, personProvider);
if (!_isDisposed && result.isSyncPersonUpdated) { if (!_isDisposed && result.isSyncPersonUpdated) {
onPeopleUpdated?.call(); onPeopleUpdated?.call();
} }
@ -32,15 +39,23 @@ class SyncController {
} }
} }
Future<void> requestResync( Future<void> requestResync({
Account account, PersonProvider personProvider) async { required Account account,
required FilesController filesController,
required PersonsController personsController,
required PersonProvider personProvider,
}) async {
if (_syncCompleter?.isCompleted == true) { if (_syncCompleter?.isCompleted == true) {
_syncCompleter = null; _syncCompleter = null;
return requestSync(account, personProvider);
} else { } else {
// already syncing // already syncing
return requestSync(account, personProvider);
} }
return requestSync(
account: account,
filesController: filesController,
personsController: personsController,
personProvider: personProvider,
);
} }
final Account account; final Account account;

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'cache_favorite.g.dart'; part 'cache_favorite.g.dart';
@ -15,18 +16,18 @@ class CacheFavorite {
/// Cache favorites using results from remote /// Cache favorites using results from remote
/// ///
/// Return number of files updated /// Return the fileIds of the affected files
Future<int> call(Account account, Iterable<int> remoteFileIds) async { Future<DbSyncIdResult> call(
Account account, Iterable<int> remoteFileIds) async {
_log.info("[call] Cache favorites"); _log.info("[call] Cache favorites");
final result = await _c.npDb.syncFavoriteFiles( final result = await _c.npDb.syncFavoriteFiles(
account: account.toDb(), account: account.toDb(),
favoriteFileIds: remoteFileIds.toList(), favoriteFileIds: remoteFileIds.toList(),
); );
final count = result.insert + result.delete + result.update; if (result.isNotEmpty) {
if (count > 0) {
KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account)); KiwiContainer().resolve<EventBus>().fire(FavoriteResyncedEvent(account));
} }
return count; return result;
} }
final DiContainer _c; final DiContainer _c;

View file

@ -7,6 +7,8 @@ import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_init.dart' as app_init; import 'package:nc_photos/app_init.dart' as app_init;
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/entity/person.dart';
import 'package:nc_photos/event/event.dart'; import 'package:nc_photos/event/event.dart';
@ -14,8 +16,11 @@ import 'package:nc_photos/use_case/person/sync_person.dart';
import 'package:nc_photos/use_case/sync_favorite.dart'; import 'package:nc_photos/use_case/sync_favorite.dart';
import 'package:nc_photos/use_case/sync_tag.dart'; import 'package:nc_photos/use_case/sync_tag.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/type.dart'; import 'package:np_common/type.dart';
import 'package:np_db/np_db.dart';
import 'package:np_platform_util/np_platform_util.dart'; import 'package:np_platform_util/np_platform_util.dart';
import 'package:to_string/to_string.dart';
part 'startup_sync.g.dart'; part 'startup_sync.g.dart';
@ -28,7 +33,11 @@ class StartupSync {
/// Sync in a background isolate /// Sync in a background isolate
static Future<SyncResult> runInIsolate( static Future<SyncResult> runInIsolate(
Account account, PersonProvider personProvider) async { Account account,
FilesController filesController,
PersonsController personsController,
PersonProvider personProvider,
) async {
return _mutex.protect(() async { return _mutex.protect(() async {
if (getRawPlatform() == NpPlatform.web) { if (getRawPlatform() == NpPlatform.web) {
// not supported on web // not supported on web
@ -42,7 +51,7 @@ class StartupSync {
final result = SyncResult.fromJson(resultJson); final result = SyncResult.fromJson(resultJson);
// events fired in background isolate won't be noticed by the main isolate, // events fired in background isolate won't be noticed by the main isolate,
// so we fire them again here // so we fire them again here
_broadcastResult(account, result); _broadcastResult(account, filesController, personsController, result);
return result; return result;
} }
}); });
@ -52,16 +61,16 @@ class StartupSync {
Account account, PersonProvider personProvider) async { Account account, PersonProvider personProvider) async {
_log.info("[_run] Begin sync"); _log.info("[_run] Begin sync");
final stopwatch = Stopwatch()..start(); final stopwatch = Stopwatch()..start();
late final int syncFavoriteCount; DbSyncIdResult? syncFavoriteResult;
late final bool isSyncPersonUpdated; DbSyncIdResult? syncTagResult;
var isSyncPersonUpdated = false;
try { try {
syncFavoriteCount = await SyncFavorite(_c)(account); syncFavoriteResult = await SyncFavorite(_c)(account);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout("[_run] Failed while SyncFavorite", e, stackTrace); _log.shout("[_run] Failed while SyncFavorite", e, stackTrace);
syncFavoriteCount = -1;
} }
try { try {
await SyncTag(_c)(account); syncTagResult = await SyncTag(_c)(account);
} catch (e, stackTrace) { } catch (e, stackTrace) {
_log.shout("[_run] Failed while SyncTag", e, stackTrace); _log.shout("[_run] Failed while SyncTag", e, stackTrace);
} }
@ -72,16 +81,28 @@ class StartupSync {
} }
_log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); _log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms");
return SyncResult( return SyncResult(
syncFavoriteCount: syncFavoriteCount, syncFavoriteResult: syncFavoriteResult,
syncTagResult: syncTagResult,
isSyncPersonUpdated: isSyncPersonUpdated, isSyncPersonUpdated: isSyncPersonUpdated,
); );
} }
static void _broadcastResult(Account account, SyncResult result) { static void _broadcastResult(
final eventBus = KiwiContainer().resolve<EventBus>(); Account account,
if (result.syncFavoriteCount > 0) { FilesController filesController,
PersonsController personsController,
SyncResult result,
) {
_$StartupSyncNpLog.log.info('[_broadcastResult] $result');
if (result.syncFavoriteResult != null) {
filesController.applySyncResult(favorites: result.syncFavoriteResult!);
// legacy
final eventBus = KiwiContainer().resolve<EventBus>();
eventBus.fire(FavoriteResyncedEvent(account)); eventBus.fire(FavoriteResyncedEvent(account));
} }
if (result.isSyncPersonUpdated) {
personsController.reload();
}
} }
final DiContainer _c; final DiContainer _c;
@ -89,23 +110,35 @@ class StartupSync {
static final _mutex = Mutex(); static final _mutex = Mutex();
} }
@toString
class SyncResult { class SyncResult {
const SyncResult({ const SyncResult({
required this.syncFavoriteCount, required this.syncFavoriteResult,
required this.syncTagResult,
required this.isSyncPersonUpdated, required this.isSyncPersonUpdated,
}); });
factory SyncResult.fromJson(JsonObj json) => SyncResult( factory SyncResult.fromJson(JsonObj json) => SyncResult(
syncFavoriteCount: json["syncFavoriteCount"], syncFavoriteResult: (json["syncFavoriteResult"] as Map?)
?.cast<String, dynamic>()
.let(DbSyncIdResult.fromJson),
syncTagResult: (json["syncTagResult"] as Map?)
?.cast<String, dynamic>()
.let(DbSyncIdResult.fromJson),
isSyncPersonUpdated: json["isSyncPersonUpdated"], isSyncPersonUpdated: json["isSyncPersonUpdated"],
); );
JsonObj toJson() => { JsonObj toJson() => {
"syncFavoriteCount": syncFavoriteCount, "syncFavoriteResult": syncFavoriteResult?.toJson(),
"syncTagResult": syncTagResult?.toJson(),
"isSyncPersonUpdated": isSyncPersonUpdated, "isSyncPersonUpdated": isSyncPersonUpdated,
}; };
final int syncFavoriteCount; @override
String toString() => _$toString();
final DbSyncIdResult? syncFavoriteResult;
final DbSyncIdResult? syncTagResult;
final bool isSyncPersonUpdated; final bool isSyncPersonUpdated;
} }

View file

@ -12,3 +12,14 @@ extension _$StartupSyncNpLog on StartupSync {
static final log = Logger("use_case.startup_sync.StartupSync"); static final log = Logger("use_case.startup_sync.StartupSync");
} }
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$SyncResultToString on SyncResult {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "SyncResult {syncFavoriteResult: $syncFavoriteResult, syncTagResult: $syncTagResult, isSyncPersonUpdated: $isSyncPersonUpdated}";
}
}

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/use_case/cache_favorite.dart'; import 'package:nc_photos/use_case/cache_favorite.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'sync_favorite.g.dart'; part 'sync_favorite.g.dart';
@ -18,7 +19,7 @@ class SyncFavorite {
/// Sync favorites in cache db with remote server /// Sync favorites in cache db with remote server
/// ///
/// Return number of files updated /// Return number of files updated
Future<int> call(Account account) async { Future<DbSyncIdResult> call(Account account) async {
_log.info("[call] Sync favorites with remote"); _log.info("[call] Sync favorites with remote");
final remote = await _getRemoteFavoriteFileIds(account); final remote = await _getRemoteFavoriteFileIds(account);
return await CacheFavorite(_c)(account, remote); return await CacheFavorite(_c)(account, remote);

View file

@ -3,6 +3,7 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
import 'package:np_db/np_db.dart';
part 'sync_tag.g.dart'; part 'sync_tag.g.dart';
@ -11,13 +12,16 @@ class SyncTag {
const SyncTag(this._c); const SyncTag(this._c);
/// Sync tags in cache db with remote server /// Sync tags in cache db with remote server
Future<void> call(Account account) async { ///
/// Return tagIds of the affected tags
Future<DbSyncIdResult> call(Account account) async {
_log.info("[call] Sync tags with remote"); _log.info("[call] Sync tags with remote");
final remote = await _c.tagRepoRemote.list(account); final remote = await _c.tagRepoRemote.list(account);
await _c.npDb.syncTags( final result = await _c.npDb.syncTags(
account: account.toDb(), account: account.toDb(),
tags: remote.map(DbTagConverter.toDb).toList(), tags: remote.map(DbTagConverter.toDb).toList(),
); );
return result;
} }
final DiContainer _c; final DiContainer _c;

View file

@ -609,7 +609,11 @@ class _HomePhotosState extends State<HomePhotos>
if (isPostSuccess) { if (isPostSuccess) {
_isScrollbarVisible = true; _isScrollbarVisible = true;
context.read<AccountController>().syncController.requestSync( context.read<AccountController>().syncController.requestSync(
widget.account, _accountPrefController.personProvider.value); account: widget.account,
filesController: context.read(),
personsController: context.read(),
personProvider: _accountPrefController.personProvider.value,
);
_tryStartMetadataTask(context); _tryStartMetadataTask(context);
} }
}); });

View file

@ -10,6 +10,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
required this.accountPrefController, required this.accountPrefController,
required this.collectionsController, required this.collectionsController,
required this.sessionController, required this.sessionController,
required this.syncController,
required this.personsController,
}) : super(_State.init( }) : super(_State.init(
zoom: prefController.homePhotosZoomLevel.value, zoom: prefController.homePhotosZoomLevel.value,
isEnableMemoryCollection: isEnableMemoryCollection:
@ -142,6 +144,12 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
memoryCollections: ev.memoryCollections, memoryCollections: ev.memoryCollections,
isLoading: _itemTransformerQueue.isProcessing, isLoading: _itemTransformerQueue.isProcessing,
)); ));
syncController.requestSync(
account: account,
filesController: controller,
personsController: personsController,
personProvider: accountPrefController.personProvider.value,
);
_tryStartMetadataTask(); _tryStartMetadataTask();
} }
@ -403,6 +411,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
final AccountPrefController accountPrefController; final AccountPrefController accountPrefController;
final CollectionsController collectionsController; final CollectionsController collectionsController;
final SessionController sessionController; final SessionController sessionController;
final SyncController syncController;
final PersonsController personsController;
final _itemTransformerQueue = final _itemTransformerQueue =
ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>();

View file

@ -18,8 +18,10 @@ import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/account_pref_controller.dart';
import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart';
import 'package:nc_photos/controller/files_controller.dart'; import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/persons_controller.dart';
import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/controller/session_controller.dart'; import 'package:nc_photos/controller/session_controller.dart';
import 'package:nc_photos/controller/sync_controller.dart';
import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart'; import 'package:nc_photos/download_handler.dart';
@ -87,6 +89,8 @@ class HomePhotos2 extends StatelessWidget {
accountPrefController: accountController.accountPrefController, accountPrefController: accountController.accountPrefController,
collectionsController: accountController.collectionsController, collectionsController: accountController.collectionsController,
sessionController: accountController.sessionController, sessionController: accountController.sessionController,
syncController: accountController.syncController,
personsController: accountController.personsController,
), ),
child: const _WrappedHomePhotos(), child: const _WrappedHomePhotos(),
); );

View file

@ -109,8 +109,12 @@ class _WrappedAccountSettingsState extends State<_WrappedAccountSettings>
if (_bloc.state.shouldResync && if (_bloc.state.shouldResync &&
_bloc.state.personProvider != PersonProvider.none) { _bloc.state.personProvider != PersonProvider.none) {
_log.fine("[dispose] Requesting to resync account"); _log.fine("[dispose] Requesting to resync account");
_accountController.syncController _accountController.syncController.requestResync(
.requestResync(_bloc.state.account, _bloc.state.personProvider); account: _bloc.state.account,
filesController: context.read(),
personsController: context.read(),
personProvider: _bloc.state.personProvider,
);
} }
_animationController.dispose(); _animationController.dispose();
super.dispose(); super.dispose();

View file

@ -49,6 +49,36 @@ class DbSyncResult {
final int update; final int update;
} }
/// Sync results with ids
///
/// The meaning of the ids returned depends on the specific call
class DbSyncIdResult {
const DbSyncIdResult({
required this.insert,
required this.delete,
required this.update,
});
factory DbSyncIdResult.fromJson(JsonObj json) => DbSyncIdResult(
insert: (json["insert"] as List).cast<int>(),
delete: (json["delete"] as List).cast<int>(),
update: (json["update"] as List).cast<int>(),
);
JsonObj toJson() => {
"insert": insert,
"delete": delete,
"update": update,
};
bool get isEmpty => insert.isEmpty && delete.isEmpty && update.isEmpty;
bool get isNotEmpty => !isEmpty;
final List<int> insert;
final List<int> delete;
final List<int> update;
}
@toString @toString
class DbLocationGroup with EquatableMixin { class DbLocationGroup with EquatableMixin {
const DbLocationGroup({ const DbLocationGroup({
@ -235,7 +265,9 @@ abstract class NpDb {
}); });
/// Add or replace nc albums in db /// Add or replace nc albums in db
Future<DbSyncResult> syncFavoriteFiles({ ///
/// Return fileIds affected by this call
Future<DbSyncIdResult> syncFavoriteFiles({
required DbAccount account, required DbAccount account,
required List<int> favoriteFileIds, required List<int> favoriteFileIds,
}); });
@ -374,7 +406,7 @@ abstract class NpDb {
}); });
/// Replace all tags for [account] /// Replace all tags for [account]
Future<DbSyncResult> syncTags({ Future<DbSyncIdResult> syncTags({
required DbAccount account, required DbAccount account,
required List<DbTag> tags, required List<DbTag> tags,
}); });

View file

@ -337,7 +337,7 @@ class NpDbSqlite implements NpDb {
} }
@override @override
Future<DbSyncResult> syncFavoriteFiles({ Future<DbSyncIdResult> syncFavoriteFiles({
required DbAccount account, required DbAccount account,
required List<int> favoriteFileIds, required List<int> favoriteFileIds,
}) async { }) async {
@ -370,10 +370,10 @@ class NpDbSqlite implements NpDb {
isFavorite: const OrNull(false), isFavorite: const OrNull(false),
); );
} }
return DbSyncResult( return DbSyncIdResult(
insert: inserts.length, insert: inserts,
delete: deletes.length, delete: deletes,
update: 0, update: const [],
); );
}); });
} }
@ -834,7 +834,7 @@ class NpDbSqlite implements NpDb {
} }
@override @override
Future<DbSyncResult> syncTags({ Future<DbSyncIdResult> syncTags({
required DbAccount account, required DbAccount account,
required List<DbTag> tags, required List<DbTag> tags,
}) async { }) async {
@ -863,10 +863,10 @@ class NpDbSqlite implements NpDb {
updates: updates, updates: updates,
); );
} }
return DbSyncResult( return DbSyncIdResult(
insert: inserts.length, insert: inserts.map((e) => e.id).toList(),
delete: deletes.length, delete: deletes.map((e) => e.id).toList(),
update: updates.length, update: updates.map((e) => e.id).toList(),
); );
}); });
} }