refactor: trakt/download hardening and cleanup sweep

This commit is contained in:
edde746
2026-04-21 13:57:03 +02:00
parent 93d196572f
commit ee020d81bd
66 changed files with 1526 additions and 2167 deletions
+50 -7
View File
@@ -1,8 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
import '../../models/trakt/trakt_ids.dart';
import '../../utils/app_logger.dart';
import 'trakt_constants.dart';
/// One pending watched/unwatched push waiting to be drained to Trakt.
@@ -73,23 +75,43 @@ class TraktSyncQueueItem {
///
/// Cap at [maxAttempts] before dropping permanently — matches
/// `OfflineWatchSyncService.maxSyncAttempts`.
///
/// Serialises all writes (`add`, `save`, `drainWith`) through a Completer chain
/// so concurrent `add()` calls don't interleave read-modify-write and lose items.
class TraktSyncQueue {
static const String _baseKey = 'trakt_sync_queue';
static const int maxAttempts = 5;
Future<void> _writeLock = Future<void>.value();
Future<T> _locked<T>(Future<T> Function() action) {
final previous = _writeLock;
final completer = Completer<void>();
_writeLock = completer.future;
return previous.then((_) => action()).whenComplete(completer.complete);
}
Future<List<TraktSyncQueueItem>> load(String userUuid) async {
final prefs = await SharedPreferences.getInstance();
final raw = prefs.getString(traktUserKey(userUuid, _baseKey));
final key = traktUserKey(userUuid, _baseKey);
final raw = prefs.getString(key);
if (raw == null) return [];
try {
final list = json.decode(raw) as List<dynamic>;
return list.map((e) => TraktSyncQueueItem.fromJson(e as Map<String, dynamic>)).toList();
} catch (_) {
} catch (e, st) {
appLogger.e('Trakt sync queue parse failed, discarding', error: e, stackTrace: st);
await prefs.setString(traktUserKey(userUuid, '${_baseKey}_corrupt'), raw);
await prefs.remove(key);
return [];
}
}
Future<void> save(String userUuid, List<TraktSyncQueueItem> items) async {
Future<void> save(String userUuid, List<TraktSyncQueueItem> items) {
return _locked(() => _saveRaw(userUuid, items));
}
Future<void> _saveRaw(String userUuid, List<TraktSyncQueueItem> items) async {
final prefs = await SharedPreferences.getInstance();
final key = traktUserKey(userUuid, _baseKey);
if (items.isEmpty) {
@@ -99,9 +121,30 @@ class TraktSyncQueue {
}
}
Future<void> add(String userUuid, TraktSyncQueueItem item) async {
final items = await load(userUuid);
items.add(item);
await save(userUuid, items);
Future<void> add(String userUuid, TraktSyncQueueItem item) {
return _locked(() async {
final items = await load(userUuid);
items.add(item);
await _saveRaw(userUuid, items);
});
}
/// Atomic drain: load the queue, run [processor] for each item, and save the
/// items the processor decided to retain. Holds the write lock for the whole
/// cycle so concurrent `add()`s wait until the drain completes (no lost items).
///
/// [processor] returns `null` to drop the item, or a (possibly mutated) item
/// to retain for the next drain (e.g. `item.incrementAttempts()`).
Future<void> drainWith(String userUuid, Future<TraktSyncQueueItem?> Function(TraktSyncQueueItem) processor) {
return _locked(() async {
final items = await load(userUuid);
if (items.isEmpty) return;
final remaining = <TraktSyncQueueItem>[];
for (final item in items) {
final keep = await processor(item);
if (keep != null) remaining.add(keep);
}
await _saveRaw(userUuid, remaining);
});
}
}