import 'dart:async'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; class StoreService { final IStoreRepository _storeRepository; final Map _cache = {}; late final StreamSubscription _storeUpdateSubscription; StoreService._({ required IStoreRepository storeRepository, }) : _storeRepository = storeRepository; // 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 /// Initializes the store with the given [storeRepository] static Future init({ required IStoreRepository storeRepository, }) async { _instance ??= await create(storeRepository: storeRepository); return _instance!; } /// Initializes the store with the given [storeRepository] static Future create({ required IStoreRepository storeRepository, }) async { final instance = StoreService._(storeRepository: storeRepository); await instance._populateCache(); instance._storeUpdateSubscription = instance._listenForChange(); return instance; } /// Fills the cache with the values from the DB Future _populateCache() async { for (StoreKey key in StoreKey.values) { final storeValue = await _storeRepository.tryGet(key); _cache[key.id] = storeValue; } } /// Listens for changes in the DB and updates the cache StreamSubscription _listenForChange() => _storeRepository.watchAll().listen((event) { _cache[event.key.id] = event.value; }); /// Disposes the store and cancels the subscription. To reuse the store call init() again void dispose() async { await _storeUpdateSubscription.cancel(); _cache.clear(); } /// Returns the stored value for the given key (possibly null) T? tryGet(StoreKey key) => _cache[key.id]; /// Returns the stored value for the given key or if null the [defaultValue] /// Throws a [StoreKeyNotFoundException] if both are null T get(StoreKey key, [T? defaultValue]) { final value = tryGet(key) ?? defaultValue; if (value == null) { throw StoreKeyNotFoundException(key); } return value; } /// Asynchronously stores the value in the DB and synchronously in the cache Future put(StoreKey key, T value) async { if (_cache[key.id] == value) return; await _storeRepository.insert(key, value); _cache[key.id] = value; } /// Watches a specific key for changes Stream watch(StoreKey key) => _storeRepository.watch(key); /// Removes the value asynchronously from the DB and synchronously from the cache Future delete(StoreKey key) async { await _storeRepository.delete(key); _cache.remove(key.id); } /// Clears all values from this 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"; }