feat: migrate store to sqlite (#21078)

* add store entity and migration

* make store service take both isar and drift repos

* migrate and switch store on beta timeline state change

* chore: make drift variables final

* dispose old store before switching repos

* use store to update values for beta timeline

* change log service to use the proper store

* migrate store when beta already enabled

* use isar repository to check beta timeline in store service

* remove unused update method from store repo

* dispose after create

* change watchAll signature in store repo

* fix test

* rename init isar to initDB

* request user to close and reopen on beta migration

* fix tests

* handle empty version in migration

* wait for cache to be populated after migration

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
shenlong
2025-08-22 01:28:50 +05:30
committed by GitHub
parent ed3997d844
commit 6f4f79d8cc
26 changed files with 7907 additions and 169 deletions

View File

@@ -6,13 +6,13 @@ 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 IsarStoreRepository _storeRepository;
final IStoreRepository _storeRepository;
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
late final StreamSubscription<StoreDto> _storeUpdateSubscription;
late final StreamSubscription<List<StoreDto>> _storeUpdateSubscription;
StoreService._({required IsarStoreRepository storeRepository}) : _storeRepository = storeRepository;
StoreService._({required IStoreRepository 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;
@@ -24,27 +24,29 @@ class StoreService {
}
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required IsarStoreRepository storeRepository}) async {
static Future<StoreService> init({required IStoreRepository storeRepository}) async {
_instance ??= await create(storeRepository: storeRepository);
return _instance!;
}
static Future<StoreService> create({required IsarStoreRepository storeRepository}) async {
final instance = StoreService._(storeRepository: storeRepository);
await instance._populateCache();
static Future<StoreService> create({required IStoreRepository storeRepository}) async {
final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache();
instance._storeUpdateSubscription = instance._listenForChange();
return instance;
}
Future<void> _populateCache() async {
Future<void> populateCache() async {
final storeValues = await _storeRepository.getAll();
for (StoreDto storeValue in storeValues) {
_cache[storeValue.key.id] = storeValue.value;
}
}
StreamSubscription<StoreDto> _listenForChange() => _storeRepository.watchAll().listen((event) {
_cache[event.key.id] = event.value;
StreamSubscription<List<StoreDto>> _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
@@ -69,7 +71,7 @@ class StoreService {
/// Stores the [value] for the [key]. Skips write if value hasn't changed.
Future<void> put<U extends StoreKey<T>, T>(U key, T value) async {
if (_cache[key.id] == value) return;
await _storeRepository.insert(key, value);
await _storeRepository.upsert(key, value);
_cache[key.id] = value;
}