feat(m4): persist the download queue across restarts

The queue/scheduler lived only in renderer memory, so saved/scheduled and other
pending downloads were silently lost on quit (audit M4). Persist them:

- main: queue.ts store (proven cached-atomic createJsonStore, queue.json) +
  queue:list / queue:save IPC; QUEUE_MAX cap.
- shared: PersistedQueueItem = the durable subset of DownloadItem (runtime-only
  progress/speed/eta/sizeLabel/finishing dropped).
- renderer: mirror the persistable items on every queue change (a signature over
  the subset means progress ticks don't re-save; main's jsonStore coalesces the
  writes) and rehydrate on launch via an App useEffect. Pure mappers extracted
  to store/queuePersist.ts and unit-tested.

Restore semantics: downloading -> queued (yt-dlp continues the .part), saved +
scheduledFor survives so the promoter fires when due, paused/error return
actionable; completed/canceled are never persisted. A queueHydrated gate stops a
launch-time store change from wiping queue.json before it's read.

Reconciles ROADMAP.md. typecheck + 268 tests + eslint + prettier green.
(OS-level quit/relaunch cycle is worth a manual smoke test.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 09:34:55 -04:00
parent 63687aaec3
commit 0a681f864f
13 changed files with 320 additions and 10 deletions
+8 -1
View File
@@ -21,7 +21,8 @@ import {
type CommandTemplate,
type YtdlpUpdateChannel,
type SystemThemeInfo,
type TaskbarProgress
type TaskbarProgress,
type PersistedQueueItem
} from '@shared/ipc'
import { PAGE_BACKGROUND } from '@shared/theme'
import { getYtdlpVersion, updateYtdlp } from './ytdlp'
@@ -36,6 +37,7 @@ import { listTemplates, saveTemplate, removeTemplate } from './templates'
import { safeOpenPath, safeShowInFolder } from './reveal'
import { openCookieLoginWindow, getCookiesStatus, clearCookies } from './cookies'
import { listErrorLog, addErrorLog, clearErrorLog } from './errorlog'
import { listQueue, saveQueue } from './queue'
import { exportBackup, importBackup } from './backup'
import {
listSources,
@@ -208,6 +210,11 @@ export function registerIpcHandlers(getMainWindow: () => BrowserWindow | null):
ipcMain.handle(IpcChannels.errorLogList, () => listErrorLog())
ipcMain.handle(IpcChannels.errorLogClear, () => clearErrorLog())
// Persisted download queue (M4): the renderer mirrors its durable items here and
// rehydrates them on launch, so saved/scheduled/pending downloads survive a quit.
ipcMain.handle(IpcChannels.queueList, () => listQueue())
ipcMain.handle(IpcChannels.queueSave, (_e, items: PersistedQueueItem[]) => saveQueue(items))
ipcMain.handle(IpcChannels.backupExport, (e) =>
exportBackup(BrowserWindow.fromWebContents(e.sender) ?? undefined)
)