fix(downloads): preserve terminal startup state

This commit is contained in:
NimBold
2026-07-15 10:23:06 +03:30
parent 82f2914077
commit 8e02a61c3f
2 changed files with 66 additions and 17 deletions
+34
View File
@@ -627,6 +627,40 @@ describe('useDownloadStore', () => {
).toHaveLength(0); ).toHaveLength(0);
}); });
it('does not restore a registration after a fast startup terminal event', async () => {
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
if (cmd === 'db_get_all_queues') return [];
if (cmd === 'db_get_all_downloads') {
return [JSON.stringify({
id: 'startup-completed',
url: 'https://example.com/file.bin',
fileName: 'file.bin',
status: 'queued',
category: 'Other',
dateAdded: '',
queueId: '00000000-0000-0000-0000-000000000001',
hasBeenDispatched: true
})];
}
if (cmd === 'enqueue_many') {
useDownloadStore.setState(state => ({
backendRegisteredIds: new Set(),
downloads: state.downloads.map(download => download.id === 'startup-completed'
? { ...download, status: 'completed' as const }
: download)
}));
return [{ id: 'startup-completed', success: true, filename: 'file.bin' }];
}
if (cmd === 'get_pending_order') return [];
return undefined;
});
await useDownloadStore.getState().initDB();
expect(useDownloadStore.getState().downloads[0].status).toBe('completed');
expect(useDownloadStore.getState().backendRegisteredIds.has('startup-completed')).toBe(false);
});
it('redownloads fallback media without requiring a format selector', async () => { it('redownloads fallback media without requiring a format selector', async () => {
useDownloadStore.setState({ useDownloadStore.setState({
downloads: [{ downloads: [{
+32 -17
View File
@@ -1176,28 +1176,43 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
.filter(result => !result.success) .filter(result => !result.success)
.map(result => [result.id, result.error || 'Backend rejected the queued download.']) .map(result => [result.id, result.error || 'Backend rejected the queued download.'])
); );
const acceptedIdSet = new Set(registeredIds);
// Commit backend ownership as soon as enqueue_many accepts an item. // Commit backend ownership as soon as enqueue_many accepts an item.
// The order query is a separate best-effort view read; if it fails, // The order query is a separate best-effort view read; if it fails,
// forgetting these registrations would let a later queue start // forgetting these registrations would let a later queue start
// enqueue the same backend lifecycle a second time. // enqueue the same backend lifecycle a second time.
set(state => ({ set(state => {
backendRegisteredIds: new Set([ // A very fast backend transfer can emit a terminal event before
...state.backendRegisteredIds, // this batch result is merged. Preserve that event's ownership
...registeredIds // cleanup instead of re-registering an already-terminal ID.
]), const liveAcceptedIds = new Set(
downloads: state.downloads.map(download => state.downloads
failedErrors.has(download.id) .filter(download =>
? { acceptedIdSet.has(download.id) &&
...download, download.status !== 'completed' &&
status: 'failed' as const, download.status !== 'failed'
lastError: failedErrors.get(download.id) )
} .map(download => download.id)
: registeredIds.includes(download.id) );
? { ...download, hasBeenDispatched: true, lastError: undefined } return {
: download backendRegisteredIds: new Set([
) ...state.backendRegisteredIds,
})); ...liveAcceptedIds
]),
downloads: state.downloads.map(download =>
failedErrors.has(download.id)
? {
...download,
status: 'failed' as const,
lastError: failedErrors.get(download.id)
}
: liveAcceptedIds.has(download.id)
? { ...download, hasBeenDispatched: true, lastError: undefined }
: download
)
};
});
try { try {
const order = await invoke('get_pending_order', { queueId: null }); const order = await invoke('get_pending_order', { queueId: null });