refactor(features): consolidate shared feature primitives

This commit is contained in:
edde746
2026-07-12 17:31:13 +02:00
parent 6506f36843
commit 7ecadffdb1
103 changed files with 1718 additions and 898 deletions
+58 -17
View File
@@ -20,32 +20,59 @@ import '../utils/isolate_helper.dart';
/// implement the abstract [getMetadata] / [pinForOffline] / [deleteForItem]
/// methods so callers can dispatch via [forBackend] instead of switching on
/// the backend type at every call site.
abstract class ApiCache {
static ApiCache? _instance;
class ApiCacheSingleton<T extends ApiCache> {
ApiCacheSingleton(this.backend, this.typeName);
/// Returns the most recently registered cache instance — used by callers
/// that don't care which backend's helpers they're hitting (e.g. plain
/// `get`/`put` from `JellyfinClient`). Backend-specific operations should
/// route through [forBackend] instead.
static ApiCache get instance {
if (_instance == null) {
throw StateError('ApiCache not initialized. Call initialize() on a backend cache first.');
final MediaBackend backend;
final String typeName;
T? _instance;
T get instance {
final value = _instance;
if (value == null) {
throw StateError('$typeName not initialized. Call $typeName.initialize() first.');
}
return _instance!;
return value;
}
/// Like [instance], but `null` before any backend cache registered —
/// for best-effort callers (nothing cached yet means nothing to clear).
static ApiCache? get maybeInstance => _instance;
void install(T instance) {
_instance = instance;
ApiCache.registerInstance(backend, instance);
}
}
/// Decodes independent cached JSON rows, dropping only the malformed row.
Map<String, MediaItem> decodeCachedMediaRows<T>(
Iterable<T> rows, {
required String Function(T row) serializedData,
required MapEntry<String, MediaItem>? Function(T row, Map<String, dynamic> json) decode,
}) {
final result = <String, MediaItem>{};
for (final row in rows) {
try {
final json = jsonDecode(serializedData(row)) as Map<String, dynamic>;
final decoded = decode(row, json);
if (decoded != null) {
result[decoded.key] = decoded.value;
}
} catch (_) {
// A malformed cache row does not invalidate its siblings.
}
}
return result;
}
abstract class ApiCache {
static final Map<MediaBackend, ApiCache> _byBackend = {};
/// Subclasses call this from their own `initialize` to register themselves
/// for backend dispatch. Also seeds [instance] so the legacy singleton
/// surface keeps working.
/// Registers a backend cache. A new database marks a new application/test
/// lifecycle, so registrations tied to the previous database are discarded
/// instead of leaving backend dispatch pointed at a closed connection.
static void registerInstance(MediaBackend backend, ApiCache cache) {
if (_byBackend.values.any((registered) => !identical(registered.database, cache.database))) {
_byBackend.clear();
}
_byBackend[backend] = cache;
_instance = cache;
}
/// Pick the cache for [backend]. Plex is the legacy default — covers items
@@ -58,6 +85,20 @@ abstract class ApiCache {
return picked;
}
/// Clears volatile rows for every distinct registered database.
///
/// Production backend caches share one [AppDatabase], while focused tests
/// may register only one backend. This operation is therefore independent
/// of backend initialization order and is a no-op before registration.
static Future<void> clearRegisteredVolatile() async {
final cleared = <AppDatabase>{};
for (final cache in _byBackend.values) {
if (cleared.add(cache.database)) {
await cache.clearVolatile();
}
}
}
final AppDatabase _db;
ApiCache(this._db);