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
+13 -2
View File
@@ -309,8 +309,19 @@ token system + shared primitives (UI/SIMP — high value, larger effort) · i18n
preload + handler + mock, zero callers).
- [x] **M3 — Remove duplicated completion side-effect in `store/downloads.ts`** (history write +
`markDownloaded` in both `applyEvent('done')` and the preview ticker). Subsumed by C2.
- [ ] **M4 — Decide queue persistence explicitly.** Scheduler/queue is renderer-memory only;
`saved`/scheduled items don't survive a quit. Persist or document as a non-goal in code.
- [x] **M4 — Decide queue persistence explicitly.** Scheduler/queue is renderer-memory only;
`saved`/scheduled items don't survive a quit. Persist or document as a non-goal in code. *Fixed
(persist, per the run decision): a main [queue.ts](src/main/queue.ts) store (the proven cached-atomic
`createJsonStore`, `queue.json`) plus `queue:list`/`queue:save` IPC. The renderer mirrors its durable
items on every queue change (a `PersistedQueueItem` = the stable subset of `DownloadItem`; runtime-only
progress/speed/eta are dropped, and a signature over the persistable subset means progress ticks don't
re-save) and rehydrates on launch via an `App` `useEffect`. On restore a `downloading` item (its process
died) normalizes back to `queued` (yt-dlp continues the `.part`), `saved`+`scheduledFor` survives so the
promoter fires it when due, `paused`/`error` return actionable; `completed`/`canceled` are never persisted.
A `queueHydrated` gate stops a launch-time store change wiping the file before it's read. Pure mappers
extracted to [queuePersist.ts](src/renderer/src/store/queuePersist.ts) and unit-tested (`test/queuePersist.test.ts`);
plumbing is typecheck-verified. **Live-verify note:** the OS-level quit→relaunch cycle itself wasn't
smoke-tested here — worth a manual confirm (schedule/park an item, quit, relaunch).*
- [x] **M5 — Command-preview feature is fully built but unwired.** `CommandPreviewResult` type +
`command:preview` channel + `previewCommand` (download.ts) + `formatCommandLine`/
`quoteForDisplay` (buildArgs) + preload method all exist, but no renderer component calls