mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-30 15:58:51 +02:00
fix(native-worker): keep batches and retries intact across seam races
- Verification challenge no longer abandons the batch: batch mates the worker cancel would mark skipped are requeued and fenced from later snapshot applications, and the adoption loop restarts the queue for requeued items when the run ends - Pause vs cancel race: a cancelled result with no pause flag set waits up to 1.5s for the pause intent to land on the main looper before being classified as a permanent skip - Stale Go cancel flag: new ResetDownloadCancel export (wired through Android and iOS bridges) drops a pre-registered cancel with no active download; retryItem/retryAllFailed call it so the first retry of a cancelled-before-start item no longer aborts instantly - Reconcile-loop errors now cancel the native worker and clear the run id before marking items failed, instead of leaving the service downloading a batch the UI already wrote off
This commit is contained in:
@@ -2566,6 +2566,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
_log.i('Retrying item: ${item.track.name} (id: $id)');
|
||||
// A cancel issued while the item never started leaves a pre-registered
|
||||
// flag in the Go backend that the next attempt would consume and abort
|
||||
// instantly; the user asked for a retry, so drop it first.
|
||||
unawaited(
|
||||
PlatformBridge.resetDownloadCancel(id).catchError((Object e) {
|
||||
_log.w('Failed to reset cancel flag for $id: $e');
|
||||
}),
|
||||
);
|
||||
_locallyCancelledItemIds.remove(id);
|
||||
_verificationRetriedItemIds.removeWhere(
|
||||
(retryKey) => retryKey == id || retryKey.startsWith('$id::'),
|
||||
@@ -2619,6 +2627,13 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
_log.i('Retrying ${failedIds.length} failed download(s)');
|
||||
for (final id in failedIds) {
|
||||
unawaited(
|
||||
PlatformBridge.resetDownloadCancel(id).catchError((Object e) {
|
||||
_log.w('Failed to reset cancel flag for $id: $e');
|
||||
}),
|
||||
);
|
||||
}
|
||||
_locallyCancelledItemIds.removeAll(failedIds);
|
||||
_pausePendingItemIds.removeAll(failedIds);
|
||||
|
||||
@@ -4055,6 +4070,14 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
);
|
||||
if (snapshot['is_running'] != true) {
|
||||
await _clearNativeWorkerRunId(runId);
|
||||
// Items may have been requeued during reconciliation (e.g. batch
|
||||
// mates of a verification challenge); hand them to the queue.
|
||||
if (!state.isPaused &&
|
||||
state.items.any(
|
||||
(item) => item.status == DownloadStatus.queued,
|
||||
)) {
|
||||
Future.microtask(() => _processQueue());
|
||||
}
|
||||
break;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(seconds: 1));
|
||||
@@ -4174,6 +4197,16 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
return false;
|
||||
}
|
||||
_log.e('Android native worker failed: $e', e, stack);
|
||||
// The native worker keeps downloading on its own; without cancelling
|
||||
// it here the batch we just marked failed would continue in the
|
||||
// background, its completions never reconciled, and the Dart queue
|
||||
// could even restart alongside it.
|
||||
try {
|
||||
await PlatformBridge.cancelNativeDownloadWorker();
|
||||
} catch (cancelError) {
|
||||
_log.w('Failed to cancel native worker after error: $cancelError');
|
||||
}
|
||||
await _clearNativeWorkerRunId(runId);
|
||||
for (final item in queuedItems) {
|
||||
final current = _findItemById(item.id);
|
||||
if (current == null ||
|
||||
@@ -4543,11 +4576,36 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_log.i(
|
||||
'Android native worker requires verification for ${current.track.name}; switching back to interactive queue',
|
||||
);
|
||||
// Cancelling the native worker marks every remaining batch item
|
||||
// "skipped" on the native side. Those items did nothing wrong —
|
||||
// remember them now and requeue them below so the batch resumes
|
||||
// after the challenge instead of being abandoned.
|
||||
final pendingBatchIds = <String>[];
|
||||
for (final pendingId in contexts.keys) {
|
||||
if (pendingId == itemId || reconciledIds.contains(pendingId)) {
|
||||
continue;
|
||||
}
|
||||
final pending = _findItemById(pendingId);
|
||||
if (pending == null) continue;
|
||||
if (pending.status == DownloadStatus.queued ||
|
||||
pending.status == DownloadStatus.downloading ||
|
||||
pending.status == DownloadStatus.finalizing) {
|
||||
pendingBatchIds.add(pendingId);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await PlatformBridge.cancelNativeDownloadWorker();
|
||||
} catch (e) {
|
||||
_log.w('Failed to cancel native worker before verification: $e');
|
||||
}
|
||||
for (final pendingId in pendingBatchIds) {
|
||||
reconciledIds.add(pendingId);
|
||||
updateItemStatus(
|
||||
pendingId,
|
||||
DownloadStatus.queued,
|
||||
progress: 0.0,
|
||||
);
|
||||
}
|
||||
await _handleVerificationRequiredDownload(
|
||||
current,
|
||||
errorMsg,
|
||||
|
||||
@@ -541,6 +541,12 @@ class PlatformBridge {
|
||||
await _channel.invokeMethod('cancelDownload', {'item_id': itemId});
|
||||
}
|
||||
|
||||
/// Drops a stale pre-registered cancel flag for an item with no active
|
||||
/// download, so a user-initiated retry does not abort instantly.
|
||||
static Future<void> resetDownloadCancel(String itemId) async {
|
||||
await _channel.invokeMethod('resetDownloadCancel', {'item_id': itemId});
|
||||
}
|
||||
|
||||
static Future<void> setDownloadDirectory(String path) async {
|
||||
await _channel.invokeMethod('setDownloadDirectory', {'path': path});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user