mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-30 05:48:59 +00:00
fix(queue): harden resume failure and legacy controls
This commit is contained in:
@@ -420,6 +420,15 @@ impl DownloadStateEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn paused_with_error(id: impl Into<String>, error: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
status: DownloadStatus::Paused.as_str().to_string(),
|
||||
error: Some(error.into()),
|
||||
file_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn completed_with_file(id: impl Into<String>, file_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
|
||||
@@ -4701,9 +4701,9 @@ async fn resume_download(
|
||||
);
|
||||
let _ = app_handle_clone.emit(
|
||||
"download-state",
|
||||
crate::ipc::DownloadStateEvent::new(
|
||||
crate::ipc::DownloadStateEvent::paused_with_error(
|
||||
&id_clone,
|
||||
crate::ipc::DownloadStatus::Paused,
|
||||
unpause_error,
|
||||
),
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { initDownloadListener, useDownloadProgressStore } from './downloadStore';
|
||||
import {
|
||||
clearDownloadControlIntents,
|
||||
downloadControlIntentFor,
|
||||
setDownloadControlIntent,
|
||||
useDownloadStore
|
||||
} from './useDownloadStore';
|
||||
@@ -427,4 +428,37 @@ describe('useDownloadProgressStore', () => {
|
||||
expect(useDownloadStore.getState().downloads[0].status).toBe('paused');
|
||||
release();
|
||||
});
|
||||
|
||||
it('accepts an authoritative resume failure while ignoring stale paused events', async () => {
|
||||
const handlers: Record<string, (event: any) => void> = {};
|
||||
vi.mocked(ipc.listenEvent).mockImplementation((event, handler) => {
|
||||
handlers[event] = handler as (event: any) => void;
|
||||
return Promise.resolve(vi.fn());
|
||||
});
|
||||
useDownloadStore.setState({
|
||||
downloads: [{
|
||||
id: 'resume-failure',
|
||||
url: 'https://example.com/file',
|
||||
fileName: 'file.bin',
|
||||
status: 'queued',
|
||||
category: 'Other',
|
||||
dateAdded: ''
|
||||
}]
|
||||
});
|
||||
setDownloadControlIntent('resume-failure', 'resume');
|
||||
|
||||
const release = await initDownloadListener();
|
||||
handlers['download-state']({ payload: {
|
||||
id: 'resume-failure',
|
||||
status: 'paused',
|
||||
error: 'aria2 resume failed'
|
||||
} });
|
||||
|
||||
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
|
||||
status: 'paused',
|
||||
lastError: 'aria2 resume failed'
|
||||
});
|
||||
expect(downloadControlIntentFor('resume-failure')).toBeUndefined();
|
||||
release();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -110,7 +110,8 @@ const startDownloadListeners = async () => {
|
||||
// row visibly paused and make dispatch reject it before IPC.
|
||||
if (status === 'paused' &&
|
||||
current.status === 'queued' &&
|
||||
downloadControlIntentFor(payload.id) === 'resume') {
|
||||
downloadControlIntentFor(payload.id) === 'resume' &&
|
||||
!payload.error) {
|
||||
// Keep the resume intent until an active or terminal event proves
|
||||
// that the new lifecycle has taken over. An explicit pause replaces
|
||||
// this intent in pauseDownload, so a genuine user pause is still
|
||||
@@ -123,6 +124,7 @@ const startDownloadListeners = async () => {
|
||||
}
|
||||
if (status === 'paused') {
|
||||
clearDownloadControlIntent(payload.id, 'pause');
|
||||
if (payload.error) clearDownloadControlIntent(payload.id, 'resume');
|
||||
}
|
||||
|
||||
// Prevent stale lifecycle events from moving a paused row back into an
|
||||
|
||||
@@ -1836,6 +1836,28 @@ describe('useDownloadStore', () => {
|
||||
expect(calls.some(call => call[0] === 'pause_download' && (call[1] as any).id === 'active')).toBe(true);
|
||||
});
|
||||
|
||||
it('direct queue controls treat missing queue ids as the main queue', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'legacy-ready', url: 'http://ready', fileName: 'ready', status: 'ready', category: 'Other', dateAdded: '' },
|
||||
{ id: 'legacy-active', url: 'http://active', fileName: 'active', status: 'processing', category: 'Other', dateAdded: '' },
|
||||
] as any[],
|
||||
});
|
||||
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'get_pending_order') return ['legacy-ready'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await expect(useDownloadStore.getState().startQueue('00000000-0000-0000-0000-000000000001'))
|
||||
.resolves.toEqual(['legacy-ready']);
|
||||
await expect(useDownloadStore.getState().pauseQueue('00000000-0000-0000-0000-000000000001'))
|
||||
.resolves.toBe(2);
|
||||
|
||||
expect(vi.mocked(ipc.invokeCommand).mock.calls.filter(([command]) => command === 'pause_download'))
|
||||
.toHaveLength(2);
|
||||
});
|
||||
|
||||
it('resumes paused items that were never dispatched through the master start action', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
|
||||
@@ -1609,7 +1609,10 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
const operation = previousOperation.catch(() => []).then(async () => {
|
||||
await waitForPendingStartupResume();
|
||||
const runnable = get().downloads
|
||||
.filter(item => item.queueId === queueId && (item.status === 'queued' || canStartDownload(item.status)))
|
||||
.filter(item =>
|
||||
(item.queueId || MAIN_QUEUE_ID) === queueId &&
|
||||
(item.status === 'queued' || canStartDownload(item.status))
|
||||
)
|
||||
.sort(queuePositionComparator);
|
||||
|
||||
if (runnable.length === 0 || !isCurrentQueueControlGeneration(queueId, requestedGeneration)) return [];
|
||||
@@ -1736,7 +1739,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => {
|
||||
advanceQueueControlGeneration(queueId);
|
||||
const activeIds = get().downloads
|
||||
.filter(item =>
|
||||
item.queueId === queueId &&
|
||||
(item.queueId || MAIN_QUEUE_ID) === queueId &&
|
||||
(canPauseDownload(item.status) || backendDispatchPromises.has(item.id))
|
||||
)
|
||||
.map(item => item.id);
|
||||
|
||||
Reference in New Issue
Block a user