import 'dart:async'; import 'package:collection/collection.dart'; import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/exception.dart'; import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/type.dart'; part 'repo2.g.dart'; abstract class AlbumRepo2 { /// Query all [Album]s defined by [albumFiles] Stream> getAlbums( Account account, List albumFiles, { ErrorWithValueHandler? onError, }); /// Create a new [album] Future create(Account account, Album album); /// Update an [album] Future update(Account account, Album album); } class BasicAlbumRepo2 implements AlbumRepo2 { const BasicAlbumRepo2(this.dataSrc); @override Stream> getAlbums( Account account, List albumFiles, { ErrorWithValueHandler? onError, }) async* { yield await dataSrc.getAlbums(account, albumFiles, onError: onError); } @override Future create(Account account, Album album) => dataSrc.create(account, album); @override Future update(Account account, Album album) => dataSrc.update(account, album); final AlbumDataSource2 dataSrc; } @npLog class CachedAlbumRepo2 implements AlbumRepo2 { const CachedAlbumRepo2(this.remoteDataSrc, this.cacheDataSrc); @override Stream> getAlbums( Account account, List albumFiles, { ErrorWithValueHandler? onError, }) async* { // get cache final cached = []; final failed = []; try { cached.addAll(await cacheDataSrc.getAlbums( account, albumFiles, onError: (f, e, stackTrace) { failed.add(f); if (e is CacheNotFoundException) { // not in cache, normal } else { _log.shout("[getAlbums] Cache failure", e, stackTrace); } }, )); yield cached; } catch (e, stackTrace) { _log.shout("[getAlbums] Failed while getAlbums", e, stackTrace); } final cachedGroup = cached.groupListsBy((c) { try { return _validateCache( c, albumFiles.firstWhere(c.albumFile!.compareServerIdentity)); } catch (_) { return false; } }); // query remote final outdated = [ ...failed, ...cachedGroup[false]?.map((e) => albumFiles.firstWhere(e.albumFile!.compareServerIdentity)) ?? const [], ]; final remote = await remoteDataSrc.getAlbums(account, outdated, onError: onError); yield (cachedGroup[true] ?? []) + remote; // update cache for (final a in remote) { unawaited(cacheDataSrc.update(account, a)); } } @override Future create(Account account, Album album) => remoteDataSrc.create(account, album); @override Future update(Account account, Album album) async { await remoteDataSrc.update(account, album); try { await cacheDataSrc.update(account, album); } catch (e, stackTrace) { _log.warning("[update] Failed to update cache", e, stackTrace); } } /// Return true if the cached album is considered up to date bool _validateCache(Album cache, File albumFile) { if (cache.albumFile!.etag?.isNotEmpty == true && cache.albumFile!.etag == albumFile.etag) { // cache is good _log.fine("[_validateCache] etag matched for ${albumFile.path}"); return true; } else { _log.info( "[_validateCache] Remote content updated for ${albumFile.path}"); return false; } } final AlbumDataSource2 remoteDataSrc; final AlbumDataSource2 cacheDataSrc; } abstract class AlbumDataSource2 { /// Query all [Album]s defined by [albumFiles] Future> getAlbums( Account account, List albumFiles, { ErrorWithValueHandler? onError, }); Future create(Account account, Album album); Future update(Account account, Album album); }