fix(downloads): profile-scoped ownership and watch-sync integrity

This commit is contained in:
edde746
2026-07-02 11:41:25 +02:00
parent 44be03d39c
commit 2b7142bdcd
8 changed files with 186 additions and 31 deletions
+16 -3
View File
@@ -372,9 +372,14 @@ class OfflineWatchSyncService extends ChangeNotifier {
try {
await _adoptLegacyWatchActionsForActiveProfile();
final profileId = _activeProfileId;
final pendingActions = profileId == null || profileId.isEmpty
? await _database.getPendingWatchActions()
: await _database.getPendingWatchActions(profileId: profileId);
if (profileId == null || profileId.isEmpty) {
// No active profile: dropping the filter would replay EVERY
// profile's queued actions through whatever clients happen to be
// bound — the wrong user's account. Actions stay queued.
appLogger.d('No active profile — deferring pending watch sync');
return;
}
final pendingActions = await _database.getPendingWatchActions(profileId: profileId);
if (pendingActions.isEmpty) {
appLogger.d('No pending watch actions to sync');
@@ -384,6 +389,14 @@ class OfflineWatchSyncService extends ChangeNotifier {
appLogger.i('Syncing ${pendingActions.length} pending watch actions');
for (final action in pendingActions) {
if (_activeProfileId != profileId) {
// A profile switch mid-loop rebinds server clients to the NEW
// user's tokens under the same server ids — replaying the rest
// would write this profile's watch history to another account.
// Remaining actions stay queued for the next sync.
appLogger.i('Active profile changed mid-sync — requeueing remaining watch actions');
return;
}
if (action.syncAttempts >= maxSyncAttempts) {
appLogger.w(
'Skipping action ${action.id} - exceeded retry limit '
+8 -4
View File
@@ -39,7 +39,10 @@ class SyncRuleResult {
class SyncRuleExecutor {
final AppDatabase _database;
bool _isExecuting = false;
DateTime? _lastFullRunAt;
/// Per profile: one profile's background pass must not consume another
/// profile's cooldown window after a switch.
final Map<String, DateTime> _lastFullRunAtByProfile = {};
static const Duration _cooldownWifi = Duration(minutes: 30);
static const Duration _cooldownCellular = Duration(hours: 3);
@@ -85,11 +88,12 @@ class SyncRuleExecutor {
return [];
}
if (!force && _lastFullRunAt != null) {
final lastFullRunAt = _lastFullRunAtByProfile[profileId];
if (!force && lastFullRunAt != null) {
final hasWifi =
connectivity.contains(ConnectivityResult.wifi) || connectivity.contains(ConnectivityResult.ethernet);
final cooldown = hasWifi ? _cooldownWifi : _cooldownCellular;
final elapsed = DateTime.now().difference(_lastFullRunAt!);
final elapsed = DateTime.now().difference(lastFullRunAt);
if (elapsed < cooldown) {
appLogger.d(
'Sync rules cooldown active (${elapsed.inMinutes}m < ${cooldown.inMinutes}m, hasWifi=$hasWifi) — skipping',
@@ -124,7 +128,7 @@ class SyncRuleExecutor {
}
}
_lastFullRunAt = DateTime.now();
_lastFullRunAtByProfile[profileId] = DateTime.now();
return results;
} finally {
_isExecuting = false;