fix(transfer): persist queue when clearing completed/failed/all (#4947)

Clear Completed, Clear Failed and Clear All mutated the Zustand store
directly, so the cleared items were never written back to localStorage.
On the next load the persisted queue restored them and they reappeared
in the Transfer Queue panel.

Route these clears through transferManager (like clearPending already
does) so each calls persistQueue() after mutating the store.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin 2026-07-06 02:01:43 +09:00 committed by GitHub
parent 9202864846
commit 3f4d4b8643
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 102 additions and 3 deletions

View file

@ -527,6 +527,90 @@ describe('TransferManager', () => {
});
});
// ── clearCompleted ───────────────────────────────────────────────
describe('clearCompleted', () => {
test('removes completed transfers, keeps others, and persists the queue', async () => {
const book1 = makeBook({ hash: 'h1', title: 'B1' });
const book2 = makeBook({ hash: 'h2', title: 'B2' });
const appService = makeAppService();
await transferManager.initialize(
appService as never,
() => [book1, book2],
vi.fn(),
translationFn,
);
transferManager.pauseQueue();
const id1 = transferManager.queueUpload(book1)!;
const id2 = transferManager.queueDownload(book2)!;
useTransferStore.getState().setTransferStatus(id1, 'completed');
transferManager.clearCompleted();
expect(useTransferStore.getState().transfers[id1]).toBeUndefined();
expect(useTransferStore.getState().transfers[id2]).toBeDefined();
const stored = JSON.parse(localStorage.getItem('readest_transfer_queue')!);
expect(stored.transfers[id1]).toBeUndefined();
expect(stored.transfers[id2]).toBeDefined();
});
});
// ── clearFailed ──────────────────────────────────────────────────
describe('clearFailed', () => {
test('removes failed/cancelled transfers, keeps others, and persists the queue', async () => {
const book1 = makeBook({ hash: 'h1', title: 'B1' });
const book2 = makeBook({ hash: 'h2', title: 'B2' });
const appService = makeAppService();
await transferManager.initialize(
appService as never,
() => [book1, book2],
vi.fn(),
translationFn,
);
transferManager.pauseQueue();
const id1 = transferManager.queueUpload(book1)!;
const id2 = transferManager.queueDownload(book2)!;
useTransferStore.getState().setTransferStatus(id1, 'failed', 'boom');
transferManager.clearFailed();
expect(useTransferStore.getState().transfers[id1]).toBeUndefined();
expect(useTransferStore.getState().transfers[id2]).toBeDefined();
const stored = JSON.parse(localStorage.getItem('readest_transfer_queue')!);
expect(stored.transfers[id1]).toBeUndefined();
expect(stored.transfers[id2]).toBeDefined();
});
});
// ── clearAll ─────────────────────────────────────────────────────
describe('clearAll', () => {
test('removes every transfer and persists the empty queue', async () => {
const book1 = makeBook({ hash: 'h1', title: 'B1' });
const book2 = makeBook({ hash: 'h2', title: 'B2' });
const appService = makeAppService();
await transferManager.initialize(
appService as never,
() => [book1, book2],
vi.fn(),
translationFn,
);
transferManager.pauseQueue();
transferManager.queueUpload(book1);
transferManager.queueDownload(book2);
transferManager.clearAll();
expect(Object.keys(useTransferStore.getState().transfers)).toHaveLength(0);
const stored = JSON.parse(localStorage.getItem('readest_transfer_queue')!);
expect(Object.keys(stored.transfers)).toHaveLength(0);
});
});
// ── Queue processing (integration-style) ─────────────────────────
describe('queue processing', () => {
test('successful upload calls appService.uploadBook and updates book', async () => {

View file

@ -67,11 +67,11 @@ export function useTransferQueue(libraryLoaded = true, delayInit = 0) {
}, []);
const clearCompleted = useCallback(() => {
useTransferStore.getState().clearCompleted();
transferManager.clearCompleted();
}, []);
const clearFailed = useCallback(() => {
useTransferStore.getState().clearFailed();
transferManager.clearFailed();
}, []);
const clearPending = useCallback(() => {
@ -79,7 +79,7 @@ export function useTransferQueue(libraryLoaded = true, delayInit = 0) {
}, []);
const clearAll = useCallback(() => {
useTransferStore.getState().clearAll();
transferManager.clearAll();
}, []);
const getTransferProgress = useCallback((bookHash: string, type: TransferType) => {

View file

@ -259,6 +259,21 @@ class TransferManager {
this.persistQueue();
}
clearCompleted(): void {
useTransferStore.getState().clearCompleted();
this.persistQueue();
}
clearFailed(): void {
useTransferStore.getState().clearFailed();
this.persistQueue();
}
clearAll(): void {
useTransferStore.getState().clearAll();
this.persistQueue();
}
private async processQueue(): Promise<void> {
if (this.isProcessing) return;