From f418234a813d2acbe9a56b82ef51e1826e0b992f Mon Sep 17 00:00:00 2001 From: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Date: Thu, 18 Jun 2026 01:29:42 +0530 Subject: [PATCH] refactor: cleanup legacy store --- .../background_sync_teardown_test.dart | 2 +- .../test_utils/general_helper.dart | 2 +- mobile/lib/domain/models/store.model.dart | 86 ---------- .../services/background_worker.service.dart | 4 +- mobile/lib/domain/services/store.service.dart | 110 ------------ mobile/lib/entities/store.entity.dart | 4 - .../repositories/app_metadata.repository.dart | 5 +- .../repositories/store.repository.dart | 78 --------- mobile/lib/infrastructure/store.dart | 105 ++++++++++++ mobile/lib/providers/auth.provider.dart | 7 +- .../infrastructure/store.provider.dart | 4 - .../services/background_upload.service.dart | 5 +- .../services/foreground_upload.service.dart | 7 +- mobile/lib/utils/bootstrap.dart | 9 +- mobile/lib/utils/isolate.dart | 4 +- mobile/lib/utils/migration.dart | 161 +++++++++--------- .../services/local_sync_service_test.dart | 6 +- .../domain/services/store_service_test.dart | 154 ----------------- .../services/sync_stream_service_test.dart | 6 +- .../repositories/store_repository_test.dart | 152 ----------------- .../sync_api_repository_test.dart | 5 +- .../test/infrastructure/repository.mock.dart | 3 - .../modules/map/map_theme_override_test.dart | 5 +- mobile/test/service.mocks.dart | 3 - mobile/test/services/action.service_test.dart | 6 +- mobile/test/services/auth.service_test.dart | 5 +- .../background_upload.service_test.dart | 9 +- .../foreground_upload.service_test.dart | 10 +- .../presentation/presentation_context.dart | 3 - 29 files changed, 227 insertions(+), 733 deletions(-) delete mode 100644 mobile/lib/domain/models/store.model.dart delete mode 100644 mobile/lib/domain/services/store.service.dart delete mode 100644 mobile/lib/entities/store.entity.dart delete mode 100644 mobile/lib/infrastructure/repositories/store.repository.dart create mode 100644 mobile/lib/infrastructure/store.dart delete mode 100644 mobile/lib/providers/infrastructure/store.provider.dart delete mode 100644 mobile/test/domain/services/store_service_test.dart delete mode 100644 mobile/test/infrastructure/repositories/store_repository_test.dart diff --git a/mobile/integration_test/background_sync_teardown_test.dart b/mobile/integration_test/background_sync_teardown_test.dart index aa0a8a9a8a..fb90ff4821 100644 --- a/mobile/integration_test/background_sync_teardown_test.dart +++ b/mobile/integration_test/background_sync_teardown_test.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'package:drift/drift.dart' show Value; +import 'package:drift/drift.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index 66955364f3..1278608cc2 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/main.dart' as app; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart deleted file mode 100644 index 80934d7d46..0000000000 --- a/mobile/lib/domain/models/store.model.dart +++ /dev/null @@ -1,86 +0,0 @@ -/// Key for each possible value in the `Store`. -/// Defines the data type for each value -enum StoreKey { - deviceId._(4), - - // Legacy keys that have been migrated to the new metadata store - legacyVersion._(0), - legacyManageLocalMediaAndroid._(137), - legacySyncMigrationStatus._(1013), - legacyAdvancedTroubleshooting._(114), - legacyEnableHapticFeedback._(126), - legacyReadonlyModeEnabled._(138), - legacyServerUrl._(10), - legacyAccessToken._(11), - legacyServerEndpoint._(12), - legacyBackupRequireCharging._(7), - legacyBackupTriggerDelay._(8), - legacySyncAlbums._(131), - legacyEnableBackup._(1003), - legacyUseWifiForUploadVideos._(1004), - legacyUseWifiForUploadPhotos._(1005), - legacySelectedAlbumSortOrder._(113), - legacySelectedAlbumSortReverse._(123), - legacyAlbumGridView._(140), - legacyAutoEndpointSwitching._(132), - legacyPreferredWifiName._(133), - legacyLocalEndpoint._(134), - legacyExternalEndpointList._(135), - legacyCustomHeaders._(127), - legacyLoopVideo._(117), - legacyLoadOriginalVideo._(136), - legacyAutoPlayVideo._(139), - legacyTapToNavigate._(141), - legacyPreferRemoteImage._(116), - legacyLoadOriginal._(101), - legacyPrimaryColor._(128), - legacyDynamicTheme._(129), - legacyColorfulInterface._(130), - legacyThemeMode._(102), - legacyCleanupKeepFavorites._(1008), - legacyCleanupKeepMediaType._(1009), - legacyCleanupKeepAlbumIds._(1010), - legacyCleanupCutoffDaysAgo._(1011), - legacyCleanupDefaultsInitialized._(1012), - legacyTilesPerRow._(103), - legacyGroupAssetsBy._(105), - legacyStorageIndicator._(109), - legacyMapRelativeDate._(119), - legacyMapShowFavoriteOnly._(118), - legacyMapIncludeArchived._(121), - legacyMapThemeMode._(124), - legacyMapwithPartners._(125), - legacyLogLevel._(115); - - const StoreKey._(this.id); - final int id; - Type get type => T; -} - -class StoreDto { - final StoreKey key; - final T? value; - - const StoreDto(this.key, this.value); - - @override - String toString() { - return ''' -StoreDto: { - key: $key, - value: ${value ?? ''}, -}'''; - } - - @override - bool operator ==(covariant StoreDto other) { - if (identical(this, other)) { - return true; - } - - return other.key == key && other.value == value; - } - - @override - int get hashCode => key.hashCode ^ value.hashCode; -} diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart index fcc54c1020..5535cdaa16 100644 --- a/mobile/lib/domain/services/background_worker.service.dart +++ b/mobile/lib/domain/services/background_worker.service.dart @@ -10,11 +10,11 @@ import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/background_worker_api.g.dart'; import 'package:immich_mobile/platform/background_worker_lock_api.g.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -375,6 +375,6 @@ Future backgroundSyncNativeEntrypoint() async { WidgetsFlutterBinding.ensureInitialized(); DartPluginRegistrant.ensureInitialized(); - final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false); await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init(); } diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart deleted file mode 100644 index 758622a43b..0000000000 --- a/mobile/lib/domain/services/store.service.dart +++ /dev/null @@ -1,110 +0,0 @@ -import 'dart:async'; - -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; - -/// Provides access to a persistent key-value store with an in-memory cache. -/// Listens for repository changes to keep the cache updated. -class StoreService { - final DriftStoreRepository _storeRepository; - - /// In-memory cache. Keys are [StoreKey.id] - final Map _cache = {}; - StreamSubscription>? _storeUpdateSubscription; - - StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository; - - // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider - static StoreService? _instance; - static StoreService get I { - if (_instance == null) { - throw UnsupportedError("StoreService not initialized. Call init() first"); - } - return _instance!; - } - - // TODO: Replace the implementation with the one from create after removing the typedef - static Future init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { - _instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates); - return _instance!; - } - - static Future create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async { - final instance = StoreService._(isarStoreRepository: storeRepository); - await instance.populateCache(); - if (listenUpdates) { - instance._storeUpdateSubscription = instance._listenForChange(); - } - return instance; - } - - Future populateCache() async { - final storeValues = await _storeRepository.getAll(); - for (StoreDto storeValue in storeValues) { - _cache[storeValue.key.id] = storeValue.value; - } - } - - StreamSubscription> _listenForChange() => _storeRepository.watchAll().listen((events) { - for (final event in events) { - _cache[event.key.id] = event.value; - } - }); - - /// Disposes the store and cancels the subscription. To reuse the store call init() again - Future dispose() async { - await _storeUpdateSubscription?.cancel(); - _storeUpdateSubscription = null; - _cache.clear(); - // Allow a subsequent init() (e.g. when a worker isolate is reused) to - // create a fresh instance instead of returning this disposed one. - if (identical(_instance, this)) { - _instance = null; - } - } - - /// Returns the cached value for [key], or `null` - T? tryGet(StoreKey key) => _cache[key.id] as T?; - - /// Returns the stored value for [key] or [defaultValue]. - /// Throws [StoreKeyNotFoundException] if value and [defaultValue] are null. - T get(StoreKey key, [T? defaultValue]) { - final value = tryGet(key) ?? defaultValue; - if (value == null) { - throw StoreKeyNotFoundException(key); - } - return value; - } - - /// Stores the [value] for the [key]. Skips write if value hasn't changed. - Future put, T>(U key, T value) async { - if (_cache[key.id] == value) { - return; - } - await _storeRepository.upsert(key, value); - _cache[key.id] = value; - } - - /// Returns a stream that emits the value for [key] on change. - Stream watch(StoreKey key) => _storeRepository.watch(key); - - /// Removes the value for [key] - Future delete(StoreKey key) async { - await _storeRepository.delete(key); - _cache.remove(key.id); - } - - /// Clears all values from the store (cache and DB) - Future clear() async { - await _storeRepository.deleteAll(); - _cache.clear(); - } -} - -class StoreKeyNotFoundException implements Exception { - final StoreKey key; - const StoreKeyNotFoundException(this.key); - - @override - String toString() => "Key - <${key.name}> not available in Store"; -} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart deleted file mode 100644 index 17ad88cee9..0000000000 --- a/mobile/lib/entities/store.entity.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:immich_mobile/domain/services/store.service.dart'; - -// ignore: non_constant_identifier_names -final Store = StoreService.I; diff --git a/mobile/lib/infrastructure/repositories/app_metadata.repository.dart b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart index 7f8faa6bb5..07a0485ad7 100644 --- a/mobile/lib/infrastructure/repositories/app_metadata.repository.dart +++ b/mobile/lib/infrastructure/repositories/app_metadata.repository.dart @@ -1,4 +1,3 @@ -import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; @@ -20,8 +19,8 @@ class AppMetadataRepository { .insertOnConflictUpdate( AppMetadataEntityCompanion.insert( key: key.name, - value: Value(key.encode(value)), - updatedAt: Value(DateTime.now()), + value: .new(key.encode(value)), + updatedAt: .new(DateTime.now()), ), ); } diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart deleted file mode 100644 index 17b084d7fc..0000000000 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ /dev/null @@ -1,78 +0,0 @@ -import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; - -class DriftStoreRepository extends DriftDatabaseRepository { - final Drift _db; - final validStoreKeys = StoreKey.values.map((e) => e.id).toSet(); - - DriftStoreRepository(super.db) : _db = db; - - Future deleteAll() async { - await _db.storeEntity.deleteAll(); - return true; - } - - Future>> getAll() async { - final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); - return query.asyncMap((entity) => _toUpdateEvent(entity)).get(); - } - - Stream>> watchAll() { - final query = _db.storeEntity.select()..where((entity) => entity.id.isIn(validStoreKeys)); - - return query.asyncMap((entity) => _toUpdateEvent(entity)).watch(); - } - - Future delete(StoreKey key) async { - await _db.storeEntity.deleteWhere((entity) => entity.id.equals(key.id)); - return; - } - - Future upsert(StoreKey key, T value) async { - await _db.storeEntity.insertOnConflictUpdate(await _fromValue(key, value)); - return true; - } - - Future tryGet(StoreKey key) async { - final entity = await _db.managers.storeEntity.filter((entity) => entity.id.equals(key.id)).getSingleOrNull(); - if (entity == null) { - return null; - } - return await _toValue(key, entity); - } - - Stream watch(StoreKey key) async* { - final query = _db.storeEntity.select()..where((entity) => entity.id.equals(key.id)); - - yield* query.watchSingleOrNull().asyncMap((e) async => e == null ? null : await _toValue(key, e)); - } - - Future> _toUpdateEvent(StoreEntityData entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id) as StoreKey; - final value = await _toValue(key, entity); - return StoreDto(key, value); - } - - Future _toValue(StoreKey key, StoreEntityData entity) async => - switch (key.type) { - const (int) => entity.intValue, - const (String) => entity.stringValue, - const (bool) => entity.intValue == 1, - const (DateTime) => entity.intValue == null ? null : DateTime.fromMillisecondsSinceEpoch(entity.intValue!), - _ => null, - } - as T?; - - Future _fromValue(StoreKey key, T value) async { - final (int? intValue, String? strValue) = switch (key.type) { - const (int) => (value as int, null), - const (String) => (null, value as String), - const (bool) => ((value as bool) ? 1 : 0, null), - const (DateTime) => ((value as DateTime).millisecondsSinceEpoch, null), - _ => throw UnsupportedError("Unsupported primitive type: ${key.type} for key: ${key.name}"), - }; - return StoreEntityCompanion(id: Value(key.id), intValue: Value(intValue), stringValue: Value(strValue)); - } -} diff --git a/mobile/lib/infrastructure/store.dart b/mobile/lib/infrastructure/store.dart new file mode 100644 index 0000000000..e368b8ad71 --- /dev/null +++ b/mobile/lib/infrastructure/store.dart @@ -0,0 +1,105 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +enum LegacyStoreKey { + deviceId(4), + legacyVersion(0), + legacyManageLocalMediaAndroid(137), + legacySyncMigrationStatus(1013), + legacyAdvancedTroubleshooting(114), + legacyEnableHapticFeedback(126), + legacyReadonlyModeEnabled(138), + legacyServerUrl(10), + legacyAccessToken(11), + legacyServerEndpoint(12), + legacyBackupRequireCharging(7), + legacyBackupTriggerDelay(8), + legacySyncAlbums(131), + legacyEnableBackup(1003), + legacyUseWifiForUploadVideos(1004), + legacyUseWifiForUploadPhotos(1005), + legacySelectedAlbumSortOrder(113), + legacySelectedAlbumSortReverse(123), + legacyAlbumGridView(140), + legacyAutoEndpointSwitching(132), + legacyPreferredWifiName(133), + legacyLocalEndpoint(134), + legacyExternalEndpointList(135), + legacyCustomHeaders(127), + legacyLoopVideo(117), + legacyLoadOriginalVideo(136), + legacyAutoPlayVideo(139), + legacyTapToNavigate(141), + legacyPreferRemoteImage(116), + legacyLoadOriginal(101), + legacyPrimaryColor(128), + legacyDynamicTheme(129), + legacyColorfulInterface(130), + legacyThemeMode(102), + legacyCleanupKeepFavorites(1008), + legacyCleanupKeepMediaType(1009), + legacyCleanupKeepAlbumIds(1010), + legacyCleanupCutoffDaysAgo(1011), + legacyCleanupDefaultsInitialized(1012), + legacyTilesPerRow(103), + legacyGroupAssetsBy(105), + legacyStorageIndicator(109), + legacyMapRelativeDate(119), + legacyMapShowFavoriteOnly(118), + legacyMapIncludeArchived(121), + legacyMapThemeMode(124), + legacyMapwithPartners(125), + legacyLogLevel(115); + + const LegacyStoreKey(this.id); + + final int id; +} + +class DeviceIdStore { + DeviceIdStore._(this._db); + + final Drift _db; + String? _deviceId; + + static DeviceIdStore? _instance; + static DeviceIdStore get I => _instance ?? (throw StateError('DeviceIdStore not initialized. Call init() first')); + + static Future init(Drift db) async { + final instance = DeviceIdStore._(db); + final row = await (db.storeEntity.select()..where((t) => t.id.equals(LegacyStoreKey.deviceId.id))) + .getSingleOrNull(); + instance._deviceId = row?.stringValue; + return _instance = instance; + } + + String? get deviceId => _deviceId; + + String get requireDeviceId => _deviceId ?? (throw StateError('deviceId not set')); + + Future setDeviceId(String value) async { + if (_deviceId == value) { + return; + } + await _db.storeEntity.insertOnConflictUpdate( + StoreEntityCompanion(id: Value(LegacyStoreKey.deviceId.id), stringValue: Value(value)), + ); + _deviceId = value; + } + + Future clear() async { + await _db.storeEntity.deleteAll(); + _deviceId = null; + } + + Future dispose() async { + _deviceId = null; + if (identical(_instance, this)) { + _instance = null; + } + } +} + +// ignore: non_constant_identifier_names +DeviceIdStore get Store => DeviceIdStore.I; diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index 97df10688f..a417c90eec 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -4,10 +4,9 @@ import 'package:flutter_udid/flutter_udid.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auth_state.model.dart'; import 'package:immich_mobile/models/auth/login_response.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -136,7 +135,7 @@ class AuthNotifier extends StateNotifier { await _widgetService.writeCredentials(serverEndpoint, accessToken, customHeaders); // Get the deviceid from the store if it exists, otherwise generate a new one - final String deviceId = Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid; + final deviceId = Store.deviceId ?? await FlutterUdid.consistentUdid; UserDto? user = await _userService.tryGetMyUser(); @@ -148,7 +147,7 @@ class AuthNotifier extends StateNotifier { // If the user information is successfully retrieved, update the store // Due to the flow of the code, this will always happen on first login user = serverUser; - await Store.put(StoreKey.deviceId, deviceId); + await Store.setDeviceId(deviceId); } } on ApiException catch (error, stackTrace) { if (error.code == 401) { diff --git a/mobile/lib/providers/infrastructure/store.provider.dart b/mobile/lib/providers/infrastructure/store.provider.dart deleted file mode 100644 index ba4d045b06..0000000000 --- a/mobile/lib/providers/infrastructure/store.provider.dart +++ /dev/null @@ -1,4 +0,0 @@ -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; - -final storeServiceProvider = Provider((_) => StoreService.I); diff --git a/mobile/lib/services/background_upload.service.dart b/mobile/lib/services/background_upload.service.dart index 86700301ea..dc5b2d84dc 100644 --- a/mobile/lib/services/background_upload.service.dart +++ b/mobile/lib/services/background_upload.service.dart @@ -8,14 +8,13 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; @@ -388,7 +387,7 @@ class BackgroundUploadService { final serverEndpoint = SessionRepository.instance.session.serverEndpoint!; final url = Uri.parse('$serverEndpoint/assets').toString(); final headers = ApiService.getRequestHeaders(); - final deviceId = Store.get(StoreKey.deviceId); + final deviceId = Store.requireDeviceId; final (baseDirectory, directory, filename) = await Task.split(filePath: file.path); final fieldsMap = { 'filename': originalFileName ?? filename, diff --git a/mobile/lib/services/foreground_upload.service.dart b/mobile/lib/services/foreground_upload.service.dart index 11cbd03017..dfe69c7301 100644 --- a/mobile/lib/services/foreground_upload.service.dart +++ b/mobile/lib/services/foreground_upload.service.dart @@ -6,14 +6,13 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/network_capability_extensions.dart'; import 'package:immich_mobile/extensions/platform_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/connectivity_api.g.dart'; import 'package:immich_mobile/providers/infrastructure/platform.provider.dart'; import 'package:immich_mobile/providers/infrastructure/storage.provider.dart'; @@ -314,7 +313,7 @@ class ForegroundUploadService { // Some apps (e.g. DJI/Fusion) return names without an extension; fall back to the asset name for those. final extension = p.extension(file.path).isNotEmpty ? p.extension(file.path) : p.extension(asset.name); final originalFileName = p.setExtension(fileName, extension); - final deviceId = Store.get(StoreKey.deviceId); + final deviceId = Store.deviceId!; final fields = { // deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818). @@ -428,7 +427,7 @@ class ForegroundUploadService { final fields = { // deviceAssetId/deviceId required by server v2.7.5 and below (drop in v4.0 per #27818). 'deviceAssetId': deviceAssetId, - 'deviceId': Store.get(StoreKey.deviceId), + 'deviceId': Store.requireDeviceId, 'fileCreatedAt': fileCreatedAt.toUtc().toIso8601String(), 'fileModifiedAt': fileModifiedAt.toUtc().toIso8601String(), 'isFavorite': 'false', diff --git a/mobile/lib/utils/bootstrap.dart b/mobile/lib/utils/bootstrap.dart index 48c3dfff12..754afb4ef2 100644 --- a/mobile/lib/utils/bootstrap.dart +++ b/mobile/lib/utils/bootstrap.dart @@ -1,7 +1,6 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/log.repository.dart'; @@ -9,7 +8,7 @@ import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.d import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/utils/debug_print.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -46,13 +45,11 @@ void configureFileDownloaderNotifications() { } abstract final class Bootstrap { - static Future<(Drift, DriftLogger)> initDomain({bool listenStoreUpdates = true, bool shouldBufferLogs = true}) async { + static Future<(Drift, DriftLogger)> initDomain({bool shouldBufferLogs = true}) async { await configureSqliteCache(); final (db, updatePool) = await openSqliteConnectionWithUpdatePool(name: 'immich'); final drift = Drift.sqlite(db, updatePool); - final DriftStoreRepository storeRepo = DriftStoreRepository(drift); - - await StoreService.init(storeRepository: storeRepo, listenUpdates: listenStoreUpdates); + await DeviceIdStore.init(drift); await SessionRepository.ensureInitialized(drift); final settingsRepo = await SettingsRepository.ensureInitialized(drift); final logDb = await _initLogger(settingsRepository: settingsRepo, shouldBufferLogs: shouldBufferLogs); diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index ab3b19b78f..5119e7bead 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -4,7 +4,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/services/log.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; import 'package:immich_mobile/utils/bootstrap.dart'; @@ -34,7 +34,7 @@ Cancelable runInIsolateGentle({ DartPluginRegistrant.ensureInitialized(); final log = Logger("IsolateLogger"); - final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false); + final (drift, logDb) = await Bootstrap.initDomain(shouldBufferLogs: false); final ref = ProviderContainer( overrides: [cancellationProvider.overrideWithValue(onCancel), driftProvider.overrideWith(driftOverride(drift))], ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index ab190acdd1..a4eacbfb9e 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -11,10 +11,8 @@ import 'package:immich_mobile/domain/models/config/app_config.dart'; import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; import 'package:immich_mobile/domain/models/settings_key.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/domain/services/feature_message.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/app_metadata.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/session.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/settings.entity.drift.dart'; @@ -23,6 +21,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/network.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; @@ -34,24 +33,22 @@ Future migrateDatabaseIfNeeded(Drift drift) async { final metadata = AppMetadataRepository(drift); final version = await metadata.get(.version); - if (version < 25) { - await _migrateTo25(); - } - - if (version < 26) { - await _migrateTo26(drift); - } - - if (version < 27) { - await _migrateTo27(drift); - } - - if (version < 28) { - await _migrateTo28(drift); - } - if (version < 29) { - await _migrateTo29(drift); + final legacyStore = await _readLegacyStore(drift); + + if (version < 25) { + await _migrateTo25(drift, legacyStore); + } + if (version < 26) { + await _migrateTo26(drift, legacyStore); + } + if (version < 27) { + await _migrateTo27(drift, legacyStore); + } + if (version < 28) { + await _migrateTo28(drift, legacyStore); + } + await _migrateTo29(drift, legacyStore); } if (isFreshInstall) { @@ -62,22 +59,29 @@ Future migrateDatabaseIfNeeded(Drift drift) async { return; } -Future _migrateTo25() async { - final accessToken = Store.tryGet(.legacyAccessToken); +Future> _readLegacyStore(Drift drift) async { + final rows = await drift.storeEntity.select().get(); + return {for (final row in rows) row.id: row.stringValue ?? row.intValue}; +} + +Future _migrateTo25(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.settings(drift, legacyStore); + + final accessToken = migrator.readLegacyStoreString(.legacyAccessToken); if (accessToken == null || accessToken.isEmpty) { return; } final urls = []; - final serverEndpoint = Store.tryGet(.legacyServerEndpoint); + final serverEndpoint = migrator.readLegacyStoreString(.legacyServerEndpoint); if (serverEndpoint != null && serverEndpoint.isNotEmpty) { urls.add(serverEndpoint); } - final localEndpoint = Store.tryGet(.legacyLocalEndpoint); + final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint); if (localEndpoint != null && localEndpoint.isNotEmpty) { urls.add(localEndpoint); } - final externalJson = Store.tryGet(.legacyExternalEndpointList); + final externalJson = migrator.readLegacyStoreString(.legacyExternalEndpointList); if (externalJson != null) { final List list = jsonDecode(externalJson); for (final entry in list) { @@ -91,7 +95,7 @@ Future _migrateTo25() async { return; } - final customHeadersStr = Store.get(.legacyCustomHeaders, ""); + final customHeadersStr = migrator.readLegacyStoreString(.legacyCustomHeaders) ?? ""; final headers = customHeadersStr.isEmpty ? const {} : (jsonDecode(customHeadersStr) as Map).cast(); @@ -99,8 +103,8 @@ Future _migrateTo25() async { await NetworkRepository.setHeaders(headers, urls, token: accessToken); } -Future _migrateTo26(Drift drift) async { - final migrator = _StoreMigrator.settings(drift); +Future _migrateTo26(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.settings(drift, legacyStore); await migrator.migrateEnumIndex(.legacyLogLevel, .logLevel, LogLevel.values); // Theme await migrator.migrateEnumName(.legacyThemeMode, .themeMode, ThemeMode.values); @@ -108,7 +112,7 @@ Future _migrateTo26(Drift drift) async { await migrator.migrateBool(.legacyDynamicTheme, .themeDynamic); await migrator.migrateBool(.legacyColorfulInterface, .themeColorfulInterface); // Cleanup - final cleanupKeepAlbumIds = await migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds); + final cleanupKeepAlbumIds = migrator.readLegacyStoreString(.legacyCleanupKeepAlbumIds); if (cleanupKeepAlbumIds != null) { final ids = cleanupKeepAlbumIds.split(',').where((id) => id.isNotEmpty).toList(); migrator.stage(.legacyCleanupKeepAlbumIds, .cleanupKeepAlbumIds, ids); @@ -137,9 +141,9 @@ Future _migrateTo26(Drift drift) async { await migrator.migrateBool(.legacyTapToNavigate, .viewerTapToNavigate); // Network await migrator.migrateBool(.legacyAutoEndpointSwitching, .networkAutoEndpointSwitching); - final preferredWifiName = await migrator.readLegacyStoreString(.legacyPreferredWifiName); + final preferredWifiName = migrator.readLegacyStoreString(.legacyPreferredWifiName); migrator.stage(.legacyPreferredWifiName, .networkPreferredWifiName, preferredWifiName); - final localEndpoint = await migrator.readLegacyStoreString(.legacyLocalEndpoint); + final localEndpoint = migrator.readLegacyStoreString(.legacyLocalEndpoint); migrator.stage(.legacyLocalEndpoint, .networkLocalEndpoint, localEndpoint); await _migrateExternalEndpointList(migrator); await _migrateCustomHeaders(migrator); @@ -157,8 +161,8 @@ Future _migrateTo26(Drift drift) async { await migrator.complete(); } -Future _migrateTo27(Drift drift) async { - final migrator = _StoreMigrator.session(drift); +Future _migrateTo27(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.session(drift, legacyStore); await migrator.migrateString(.legacyServerUrl, .serverUrl); await migrator.migrateString(.legacyAccessToken, .accessToken); await migrator.migrateString(.legacyServerEndpoint, .serverEndpoint); @@ -167,8 +171,8 @@ Future _migrateTo27(Drift drift) async { await SessionRepository.instance.refresh(); } -Future _migrateTo28(Drift drift) async { - final migrator = _StoreMigrator.settings(drift); +Future _migrateTo28(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.settings(drift, legacyStore); await migrator.migrateBool(.legacyAdvancedTroubleshooting, .advancedTroubleshooting); await migrator.migrateBool(.legacyEnableHapticFeedback, .advancedEnableHapticFeedback); await migrator.migrateBool(.legacyReadonlyModeEnabled, .advancedReadonlyModeEnabled); @@ -177,10 +181,10 @@ Future _migrateTo28(Drift drift) async { await SettingsRepository.instance.refresh(); } -Future _migrateTo29(Drift drift) async { - final migrator = _StoreMigrator.appMetadata(drift); +Future _migrateTo29(Drift drift, Map legacyStore) async { + final migrator = _StoreMigrator.appMetadata(drift, legacyStore); - final rawStatus = await migrator.readLegacyStoreString(.legacySyncMigrationStatus); + final rawStatus = migrator.readLegacyStoreString(.legacySyncMigrationStatus); if (rawStatus != null) { final decoded = jsonDecode(rawStatus); final migrations = decoded is List ? decoded.whereType().toList() : []; @@ -192,7 +196,7 @@ Future _migrateTo29(Drift drift) async { } Future _migrateAlbumSortMode(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder); + final raw = migrator.readLegacyStoreInt(.legacySelectedAlbumSortOrder); final mode = AlbumSortMode.values.firstWhereOrNull((e) => raw != null && e.storeIndex == raw); if (mode == null) { return; @@ -202,7 +206,7 @@ Future _migrateAlbumSortMode(_StoreMigrator migrator) async { } Future _migrateExternalEndpointList(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(.legacyExternalEndpointList); + final raw = migrator.readLegacyStoreString(.legacyExternalEndpointList); if (raw == null) { return; } @@ -222,11 +226,11 @@ Future _migrateExternalEndpointList(_StoreMigrator migrator) // ignore invalid entries } - migrator.stage(StoreKey.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); + migrator.stage(.legacyExternalEndpointList, SettingsKey.networkExternalEndpointList, urls); } Future _migrateCustomHeaders(_StoreMigrator migrator) async { - final raw = await migrator.readLegacyStoreString(.legacyCustomHeaders); + final raw = migrator.readLegacyStoreString(.legacyCustomHeaders); if (raw == null) { return; } @@ -245,14 +249,21 @@ Future _migrateCustomHeaders(_StoreMigrator migrator) async { // ignore invalid entries } - migrator.stage(StoreKey.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); + migrator.stage(.legacyCustomHeaders, SettingsKey.networkCustomHeaders, headers); } class _StoreMigrator { - _StoreMigrator._(this._db, {required this.encode, required this.readDefault, required this.insertRow}); + _StoreMigrator._( + this._db, + this._legacyStore, { + required this.encode, + required this.readDefault, + required this.insertRow, + }); - static _StoreMigrator settings(Drift db) => _StoreMigrator._( + static _StoreMigrator settings(Drift db, Map legacyStore) => _StoreMigrator._( db, + legacyStore, encode: (key, value) => key.encode(value), readDefault: (key) => defaultConfig.read(key), insertRow: (batch, name, value) => batch.insert( @@ -262,8 +273,9 @@ class _StoreMigrator { ), ); - static _StoreMigrator session(Drift db) => _StoreMigrator._( + static _StoreMigrator session(Drift db, Map legacyStore) => _StoreMigrator._( db, + legacyStore, encode: (key, value) => key.encode(value), readDefault: (key) => defaultSession.read(key), insertRow: (batch, name, value) => batch.insert( @@ -273,26 +285,29 @@ class _StoreMigrator { ), ); - static _StoreMigrator appMetadata(Drift db) => _StoreMigrator._( - db, - encode: (key, value) => key.encode(value), - readDefault: (_) => null, - insertRow: (batch, name, value) => batch.insert( - db.appMetadataEntity, - AppMetadataEntityCompanion(key: Value(name), value: Value(value)), - mode: InsertMode.insertOrReplace, - ), - ); + static _StoreMigrator appMetadata(Drift db, Map legacyStore) => + _StoreMigrator._( + db, + legacyStore, + encode: (key, value) => key.encode(value), + readDefault: (_) => null, + insertRow: (batch, name, value) => batch.insert( + db.appMetadataEntity, + AppMetadataEntityCompanion(key: Value(name), value: Value(value)), + mode: InsertMode.insertOrReplace, + ), + ); final Drift _db; + final Map _legacyStore; final String Function(K key, Object value) encode; final Object? Function(K key) readDefault; final void Function(Batch batch, String name, String? value) insertRow; final Map _cache = {}; final List _migratedStoreIds = []; - Future migrateEnumIndex(StoreKey legacyKey, K newKey, List values) async { - final index = await readLegacyStoreInt(legacyKey); + Future migrateEnumIndex(LegacyStoreKey legacyKey, K newKey, List values) async { + final index = readLegacyStoreInt(legacyKey); if (index == null) { return; } @@ -306,8 +321,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateEnumName(StoreKey legacyKey, K newKey, List values) async { - final name = await readLegacyStoreString(legacyKey); + Future migrateEnumName(LegacyStoreKey legacyKey, K newKey, List values) async { + final name = readLegacyStoreString(legacyKey); if (name == null) { return; } @@ -321,8 +336,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateBool(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey); + Future migrateBool(LegacyStoreKey legacyKey, K newKey) async { + final intValue = readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -331,8 +346,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateInt(StoreKey legacyKey, K newKey) async { - final intValue = await readLegacyStoreInt(legacyKey); + Future migrateInt(LegacyStoreKey legacyKey, K newKey) async { + final intValue = readLegacyStoreInt(legacyKey); if (intValue == null) { return; } @@ -341,8 +356,8 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateString(StoreKey legacyKey, K newKey) async { - final value = await readLegacyStoreString(legacyKey); + Future migrateString(LegacyStoreKey legacyKey, K newKey) async { + final value = readLegacyStoreString(legacyKey); if (value == null || value.isEmpty) { return; } @@ -351,12 +366,12 @@ class _StoreMigrator { _migratedStoreIds.add(legacyKey.id); } - Future migrateNullableString(StoreKey legacyKey, K newKey) async { - _cache[newKey] = await readLegacyStoreString(legacyKey); + Future migrateNullableString(LegacyStoreKey legacyKey, K newKey) async { + _cache[newKey] = readLegacyStoreString(legacyKey); _migratedStoreIds.add(legacyKey.id); } - void stage(StoreKey legacyKey, K newKey, Object? value) { + void stage(LegacyStoreKey legacyKey, K newKey, Object? value) { _cache[newKey] = value; _migratedStoreIds.add(legacyKey.id); } @@ -375,15 +390,9 @@ class _StoreMigrator { await deleteLegacyStoreRows(_migratedStoreIds); } - Future readLegacyStoreString(StoreKey key) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); - return row?.stringValue; - } + String? readLegacyStoreString(LegacyStoreKey key) => _legacyStore[key.id] as String?; - Future readLegacyStoreInt(StoreKey key) async { - final row = await (_db.storeEntity.select()..where((t) => t.id.equals(key.id))).getSingleOrNull(); - return row?.intValue; - } + int? readLegacyStoreInt(LegacyStoreKey key) => _legacyStore[key.id] as int?; Future deleteLegacyStoreRows(List ids) async { if (ids.isEmpty) { diff --git a/mobile/test/domain/services/local_sync_service_test.dart b/mobile/test/domain/services/local_sync_service_test.dart index cdc45f7d09..61c7d3e513 100644 --- a/mobile/test/domain/services/local_sync_service_test.dart +++ b/mobile/test/domain/services/local_sync_service_test.dart @@ -5,13 +5,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/services/local_sync.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/trashed_local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,7 +35,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart deleted file mode 100644 index ca84fe04a2..0000000000 --- a/mobile/test/domain/services/store_service_test.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; -import 'package:mocktail/mocktail.dart'; - -import '../../infrastructure/repository.mock.dart'; - -const _kAccessToken = '#ThisIsAToken'; -const _kAdvancedTroubleshooting = false; -const _kVersion = 2; - -void main() { - late StoreService sut; - late DriftStoreRepository mockDriftStoreRepo; - late StreamController>> controller; - - setUp(() async { - controller = StreamController>>.broadcast(); - mockDriftStoreRepo = MockDriftStoreRepository(); - // For generics, we need to provide fallback to each concrete type to avoid runtime errors - registerFallbackValue(StoreKey.legacyAccessToken); - registerFallbackValue(StoreKey.legacyVersion); - registerFallbackValue(StoreKey.legacyAdvancedTroubleshooting); - - when(() => mockDriftStoreRepo.getAll()).thenAnswer( - (_) async => [ - const StoreDto(StoreKey.legacyAccessToken, _kAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kAdvancedTroubleshooting), - const StoreDto(StoreKey.legacyVersion, _kVersion), - ], - ); - when(() => mockDriftStoreRepo.watchAll()).thenAnswer((_) => controller.stream); - - sut = await StoreService.create(storeRepository: mockDriftStoreRepo); - }); - - tearDown(() async { - unawaited(sut.dispose()); - await controller.close(); - }); - - group("Store Service Init:", () { - test('Populates the internal cache on init', () { - verify(() => mockDriftStoreRepo.getAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken); - expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), _kAdvancedTroubleshooting); - expect(sut.tryGet(StoreKey.legacyVersion), _kVersion); - // Other keys should be null - expect(sut.tryGet(StoreKey.deviceId), isNull); - }); - - test('Listens to stream of store updates', () async { - final event = StoreDto(StoreKey.legacyAccessToken, _kAccessToken.toUpperCase()); - controller.add([event]); - - await pumpEventQueue(); - - verify(() => mockDriftStoreRepo.watchAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), _kAccessToken.toUpperCase()); - }); - }); - - group('Store Service get:', () { - test('Returns the stored value for the given key', () { - expect(sut.get(StoreKey.legacyAccessToken), _kAccessToken); - }); - - test('Throws StoreKeyNotFoundException for nonexistent keys', () { - expect(() => sut.get(StoreKey.deviceId), throwsA(isA())); - }); - - test('Returns the stored value for the given key or the defaultValue', () { - expect(sut.get(StoreKey.legacyBackupTriggerDelay, 5), 5); - }); - }); - - group('Store Service put:', () { - setUp(() { - when(() => mockDriftStoreRepo.upsert(any>(), any())).thenAnswer((_) async => true); - }); - - test('Skip insert when value is not modified', () async { - await sut.put(StoreKey.legacyAccessToken, _kAccessToken); - verifyNever(() => mockDriftStoreRepo.upsert(StoreKey.legacyAccessToken, any())); - }); - - test('Insert value when modified', () async { - final newAccessToken = _kAccessToken.toUpperCase(); - await sut.put(StoreKey.legacyAccessToken, newAccessToken); - verify(() => mockDriftStoreRepo.upsert(StoreKey.legacyAccessToken, newAccessToken)).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), newAccessToken); - }); - }); - - group('Store Service watch:', () { - late StreamController valueController; - - setUp(() { - valueController = StreamController.broadcast(); - when(() => mockDriftStoreRepo.watch(any>())).thenAnswer((_) => valueController.stream); - }); - - tearDown(() async { - await valueController.close(); - }); - - test('Watches a specific key for changes', () async { - final stream = sut.watch(StoreKey.legacyAccessToken); - final events = [_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()]; - - unawaited(expectLater(stream, emitsInOrder(events))); - - for (final event in events) { - valueController.add(event); - } - - await pumpEventQueue(); - verify(() => mockDriftStoreRepo.watch(StoreKey.legacyAccessToken)).called(1); - }); - }); - - group('Store Service delete:', () { - setUp(() { - when(() => mockDriftStoreRepo.delete(any>())).thenAnswer((_) async => true); - }); - - test('Removes the value from the DB', () async { - await sut.delete(StoreKey.legacyAccessToken); - verify(() => mockDriftStoreRepo.delete(StoreKey.legacyAccessToken)).called(1); - }); - - test('Removes the value from the cache', () async { - await sut.delete(StoreKey.legacyAccessToken); - expect(sut.tryGet(StoreKey.legacyAccessToken), isNull); - }); - }); - - group('Store Service clear:', () { - setUp(() { - when(() => mockDriftStoreRepo.deleteAll()).thenAnswer((_) async => true); - }); - - test('Clears all values from the store', () async { - await sut.clear(); - verify(() => mockDriftStoreRepo.deleteAll()).called(1); - expect(sut.tryGet(StoreKey.legacyAccessToken), isNull); - expect(sut.tryGet(StoreKey.legacyAdvancedTroubleshooting), isNull); - expect(sut.tryGet(StoreKey.legacyVersion), isNull); - }); - }); -} diff --git a/mobile/test/domain/services/sync_stream_service_test.dart b/mobile/test/domain/services/sync_stream_service_test.dart index e6cadda310..4320d94ba7 100644 --- a/mobile/test/domain/services/sync_stream_service_test.dart +++ b/mobile/test/domain/services/sync_stream_service_test.dart @@ -7,15 +7,13 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/app_metadata_key.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/sync_stream.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.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/infrastructure/repositories/trashed_local_asset.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; @@ -63,7 +61,7 @@ void main() { registerFallbackValue(const []); db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart deleted file mode 100644 index 2d3bda5fab..0000000000 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ /dev/null @@ -1,152 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart' hide isNull; -import 'package:drift/native.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/infrastructure/entities/store.entity.drift.dart'; -import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; - -const _kTestAccessToken = "#TestToken"; -const _kTestVersion = 10; -const _kTestAdvancedTroubleshooting = false; - -Future _populateStore(Drift db) async { - await db.batch((batch) async { - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyAdvancedTroubleshooting.id), - intValue: const Value(_kTestAdvancedTroubleshooting ? 1 : 0), - stringValue: const Value(null), - ), - ); - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyAccessToken.id), - intValue: const Value(null), - stringValue: const Value(_kTestAccessToken), - ), - ); - batch.insert( - db.storeEntity, - StoreEntityCompanion( - id: Value(StoreKey.legacyVersion.id), - intValue: const Value(_kTestVersion), - stringValue: const Value(null), - ), - ); - }); -} - -void main() { - late Drift db; - late DriftStoreRepository sut; - - setUp(() async { - db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - sut = DriftStoreRepository(db); - }); - - tearDown(() async { - await db.close(); - }); - - group('Store Repository converters:', () { - test('converts int', () async { - int? version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, isNull); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion); - version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion); - }); - - test('converts string', () async { - String? accessToken = await sut.tryGet(StoreKey.legacyAccessToken); - expect(accessToken, isNull); - await sut.upsert(StoreKey.legacyAccessToken, _kTestAccessToken); - accessToken = await sut.tryGet(StoreKey.legacyAccessToken); - expect(accessToken, _kTestAccessToken); - }); - - test('converts bool', () async { - bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isNull); - await sut.upsert(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting); - advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, _kTestAdvancedTroubleshooting); - }); - }); - - group('Store Repository Deletes:', () { - setUp(() async { - await _populateStore(db); - }); - - test('delete()', () async { - bool? advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isFalse); - await sut.delete(StoreKey.legacyAdvancedTroubleshooting); - advancedTroubleshooting = await sut.tryGet(StoreKey.legacyAdvancedTroubleshooting); - expect(advancedTroubleshooting, isNull); - }); - - test('deleteAll()', () async { - final count = await db.storeEntity.count().getSingle(); - expect(count, isNot(isZero)); - await sut.deleteAll(); - unawaited(expectLater(await db.storeEntity.count().getSingle(), isZero)); - }); - }); - - group('Store Repository Updates:', () { - setUp(() async { - await _populateStore(db); - }); - - test('upsert()', () async { - int? version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - version = await sut.tryGet(StoreKey.legacyVersion); - expect(version, _kTestVersion + 10); - }); - }); - - group('Store Repository Watchers:', () { - setUp(() async { - await _populateStore(db); - }); - - test('watch()', () async { - final stream = sut.watch(StoreKey.legacyVersion); - unawaited(expectLater(stream, emitsInOrder([_kTestVersion, _kTestVersion + 10]))); - await pumpEventQueue(); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - }); - - test('watchAll()', () async { - final stream = sut.watchAll(); - unawaited( - expectLater( - stream, - emitsInOrder([ - [ - const StoreDto(StoreKey.legacyVersion, _kTestVersion), - const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), - ], - [ - const StoreDto(StoreKey.legacyVersion, _kTestVersion + 10), - const StoreDto(StoreKey.legacyAccessToken, _kTestAccessToken), - const StoreDto(StoreKey.legacyAdvancedTroubleshooting, _kTestAdvancedTroubleshooting), - ], - ]), - ), - ); - await sut.upsert(StoreKey.legacyVersion, _kTestVersion + 10); - }); - }); -} diff --git a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart index 3a3e40cbae..ffb9abb15c 100644 --- a/mobile/test/infrastructure/repositories/sync_api_repository_test.dart +++ b/mobile/test/infrastructure/repositories/sync_api_repository_test.dart @@ -6,10 +6,9 @@ import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:immich_mobile/domain/models/sync_event.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/utils/semver.dart'; import 'package:mocktail/mocktail.dart'; import 'package:openapi/api.dart'; @@ -41,7 +40,7 @@ void main() { setUpAll(() async { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); }); setUp(() { diff --git a/mobile/test/infrastructure/repository.mock.dart b/mobile/test/infrastructure/repository.mock.dart index c0c511bce0..f8733af33d 100644 --- a/mobile/test/infrastructure/repository.mock.dart +++ b/mobile/test/infrastructure/repository.mock.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/infrastructure/repositories/remote_album.repositor import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_api.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_migration.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart'; @@ -19,8 +18,6 @@ import 'package:immich_mobile/repositories/drift_album_api_repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:mocktail/mocktail.dart'; -class MockDriftStoreRepository extends Mock implements DriftStoreRepository {} - class MockSettingsRepository extends Mock implements SettingsRepository {} class MockAppMetadataRepository extends Mock implements AppMetadataRepository {} diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index 817ce0c4b1..b74274b44f 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -6,9 +6,8 @@ import 'package:drift/native.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/map/map_state.model.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; @@ -32,7 +31,7 @@ void main() { setUp(() async { mapState = const MapState(themeMode: ThemeMode.dark); mapStateNotifier = MockMapStateNotifier(mapState); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); overrides = [ mapStateNotifierProvider.overrideWith(() => mapStateNotifier), localeProvider.overrideWithValue(const Locale("en")), diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart index 7e67d4d250..b340020319 100644 --- a/mobile/test/service.mocks.dart +++ b/mobile/test/service.mocks.dart @@ -2,7 +2,6 @@ import 'package:immich_mobile/domain/services/asset.service.dart'; import 'package:immich_mobile/domain/services/background_worker.service.dart'; import 'package:immich_mobile/domain/services/partner.service.dart'; import 'package:immich_mobile/domain/services/remote_album.service.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/domain/services/user.service.dart'; import 'package:immich_mobile/domain/utils/background_sync.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; @@ -23,8 +22,6 @@ class MockApiService extends Mock implements ApiService {} class MockNetworkService extends Mock implements NetworkService {} -class MockStoreService extends Mock implements StoreService {} - class MockNativeSyncApi extends Mock implements NativeSyncApi {} class MockPartnerService extends Mock implements PartnerService {} diff --git a/mobile/test/services/action.service_test.dart b/mobile/test/services/action.service_test.dart index 2d90e24503..b74e3fb8e3 100644 --- a/mobile/test/services/action.service_test.dart +++ b/mobile/test/services/action.service_test.dart @@ -2,10 +2,8 @@ import 'package:drift/drift.dart' as drift; import 'package:drift/native.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/services/action.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -25,7 +23,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; db = Drift(drift.DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); }); tearDownAll(() async { diff --git a/mobile/test/services/auth.service_test.dart b/mobile/test/services/auth.service_test.dart index 9f59f2ee91..d298ad56cf 100644 --- a/mobile/test/services/auth.service_test.dart +++ b/mobile/test/services/auth.service_test.dart @@ -2,10 +2,9 @@ import 'package:drift/drift.dart' hide isNull; import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/services/auth.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,7 +36,7 @@ void main() { setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); await SessionRepository.ensureInitialized(db); }); diff --git a/mobile/test/services/background_upload.service_test.dart b/mobile/test/services/background_upload.service_test.dart index b084277a71..c5f6c4c7fd 100644 --- a/mobile/test/services/background_upload.service_test.dart +++ b/mobile/test/services/background_upload.service_test.dart @@ -8,13 +8,10 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/session.model.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/services/background_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -39,11 +36,11 @@ void main() { (MethodCall methodCall) async => 'test', ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); await SettingsRepository.ensureInitialized(db); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(SessionKey.serverEndpoint, 'https://demo.immich.app'); - await Store.put(StoreKey.deviceId, 'test-device-id'); + await Store.setDeviceId('test-device-id'); }); setUp(() { diff --git a/mobile/test/services/foreground_upload.service_test.dart b/mobile/test/services/foreground_upload.service_test.dart index a85196d30c..2cc8857b08 100644 --- a/mobile/test/services/foreground_upload.service_test.dart +++ b/mobile/test/services/foreground_upload.service_test.dart @@ -4,13 +4,10 @@ import 'package:drift/drift.dart' hide isNotNull, isNull; import 'package:drift/native.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/store.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; +import 'package:immich_mobile/infrastructure/store.dart'; import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/services/foreground_upload.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -37,12 +34,11 @@ void main() { (MethodCall methodCall) async => 'test', ); db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db)); + await DeviceIdStore.init(db); await SettingsRepository.ensureInitialized(db); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(.serverEndpoint, 'http://demo.immich.app'); - await Store.put(StoreKey.deviceId, 'device-id'); - + await DeviceIdStore.I.setDeviceId('device-id'); registerFallbackValue(File('file')); registerFallbackValue({}); }); diff --git a/mobile/test/unit/presentation/presentation_context.dart b/mobile/test/unit/presentation/presentation_context.dart index fb7e874e78..f796030d34 100644 --- a/mobile/test/unit/presentation/presentation_context.dart +++ b/mobile/test/unit/presentation/presentation_context.dart @@ -7,12 +7,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; -import 'package:immich_mobile/domain/services/store.service.dart'; import 'package:immich_mobile/generated/codegen_loader.g.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/session.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart'; -import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'; import 'package:immich_mobile/presentation/actions/action.dart'; import 'package:immich_mobile/presentation/actions/action.widget.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; @@ -74,7 +72,6 @@ class PresentationContext { TestUtils.init(); if (_db == null) { final db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true)); - await StoreService.init(storeRepository: DriftStoreRepository(db), listenUpdates: false); await SessionRepository.ensureInitialized(db); await SessionRepository.instance.write(.serverEndpoint, serverEndpoint); _db = db;