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
3 changed files with 102 additions and 3 deletions
@@ -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 () => {
@@ -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) => {
@@ -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;