refactor(mobile): split store into repo and service (#16199)

* refactor(mobile): migrate store

* refactor(mobile): expand abbreviations

* chore(mobile): fix lint

---------

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-02-20 00:35:24 +05:30
committed by GitHub
parent 8634c59850
commit aeb3e0a84f
33 changed files with 582 additions and 287 deletions
+34
View File
@@ -0,0 +1,34 @@
# Domain Layer
This directory contains the domain layer of Immich. The domain layer is responsible for the business logic of the app. It includes interfaces for repositories, models, services and utilities. This layer should never depend on anything from the presentation layer or from the infrastructure layer.
## Structure
- **[Interfaces](./interfaces/)**: These are the interfaces that define the contract for data operations.
- **[Models](./models/)**: These are the core data classes that represent the business models.
- **[Services](./services/)**: These are the classes that contain the business logic and interact with the repositories.
- **[Utils](./utils/)**: These are utility classes and functions that provide common functionalities used across the domain layer.
```
domain/
├── interfaces/
│ └── user.interface.dart
├── models/
│ └── user.model.dart
├── services/
│ └── user.service.dart
└── utils/
└── date_utils.dart
```
## Usage
The domain layer provides services that implement the business logic by consuming repositories through dependency injection. Services are exposed through Riverpod providers in the root `providers` directory.
```dart
// In presentation layer
final userService = ref.watch(userServiceProvider);
final user = await userService.getUser(userId);
```
The presentation layer should never directly use repositories, but instead interact with the domain layer through services.
@@ -0,0 +1,3 @@
abstract interface class IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback);
}
@@ -0,0 +1,17 @@
import 'package:immich_mobile/entities/store.entity.dart';
abstract interface class IStoreRepository {
Future<bool> insert<T>(StoreKey<T> key, T value);
Future<T?> tryGet<T>(StoreKey<T> key);
Stream<T?> watch<T>(StoreKey<T> key);
Stream<StoreUpdateEvent> watchAll();
Future<bool> update<T>(StoreKey<T> key, T value);
Future<void> delete<T>(StoreKey<T> key);
Future<void> deleteAll();
}
@@ -0,0 +1,106 @@
import 'dart:async';
import 'package:immich_mobile/domain/interfaces/store.interface.dart';
import 'package:immich_mobile/entities/store.entity.dart';
class StoreService {
final IStoreRepository _storeRepository;
final Map<int, dynamic> _cache = {};
late final StreamSubscription<StoreUpdateEvent> _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<StoreService> init({
required IStoreRepository storeRepository,
}) async {
_instance ??= await create(storeRepository: storeRepository);
return _instance!;
}
/// Initializes the store with the given [storeRepository]
static Future<StoreService> 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<void> _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<StoreUpdateEvent> _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<T>(StoreKey<T> 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<T>(StoreKey<T> 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<void> put<T>(StoreKey<T> 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<T?> watch<T>(StoreKey<T> key) => _storeRepository.watch(key);
/// Removes the value asynchronously from the DB and synchronously from the cache
Future<void> delete<T>(StoreKey<T> key) async {
await _storeRepository.delete(key);
_cache.remove(key.id);
}
/// Clears all values from this store (cache and DB)
Future<void> 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";
}