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);
});
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 () => {
useDownloadStore.setState({
downloads: [{
+32 -17
View File
@@ -1176,28 +1176,43 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
.filter(result => !result.success)
.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.
// The order query is a separate best-effort view read; if it fails,
// forgetting these registrations would let a later queue start
// enqueue the same backend lifecycle a second time.
set(state => ({
backendRegisteredIds: new Set([
...state.backendRegisteredIds,
...registeredIds
]),
downloads: state.downloads.map(download =>
failedErrors.has(download.id)
? {
...download,
status: 'failed' as const,
lastError: failedErrors.get(download.id)
}
: registeredIds.includes(download.id)
? { ...download, hasBeenDispatched: true, lastError: undefined }
: download
)
}));
set(state => {
// A very fast backend transfer can emit a terminal event before
// this batch result is merged. Preserve that event's ownership
// cleanup instead of re-registering an already-terminal ID.
const liveAcceptedIds = new Set(
state.downloads
.filter(download =>
acceptedIdSet.has(download.id) &&
download.status !== 'completed' &&
download.status !== 'failed'
)
.map(download => download.id)
);
return {
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 {
const order = await invoke('get_pending_order', { queueId: null });