Files
AeroFetch/src/renderer/src/store/queuePersist.ts
T
debont80 0a681f864f 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>
2026-07-01 10:25:58 -04:00

64 lines
2.0 KiB
TypeScript

import { type PersistedQueueItem, type PersistedQueueStatus } from '@shared/ipc'
import { type DownloadItem, type DownloadStatus } from './downloadTypes'
/**
* Pure mapping between the live `DownloadItem` and the durable `PersistedQueueItem`
* main stores across restarts (M4). Kept free of any store/zustand import so it can
* be unit-tested in isolation (mirrors queueStats / formatters). Only stable fields
* persist — runtime-only progress/speed/eta/sizeLabel/finishing are dropped and
* re-derived on restore.
*/
// Statuses worth surviving a quit. 'completed'/'canceled' are transient results
// (completions already live in history) and are never persisted.
export const PERSISTED_STATUSES: readonly DownloadStatus[] = [
'queued',
'downloading',
'paused',
'saved',
'error'
]
export function toPersisted(i: DownloadItem): PersistedQueueItem {
return {
id: i.id,
url: i.url,
title: i.title,
channel: i.channel,
durationLabel: i.durationLabel,
thumbnail: i.thumbnail,
kind: i.kind,
quality: i.quality,
status: i.status as PersistedQueueStatus,
filePath: i.filePath,
error: i.error,
formatId: i.formatId,
formatHasAudio: i.formatHasAudio,
options: i.options,
extraArgs: i.extraArgs,
trim: i.trim,
scheduledFor: i.scheduledFor,
incognito: i.incognito,
probedMeta: i.probedMeta,
collection: i.collection,
mediaItemId: i.mediaItemId
}
}
export function fromPersisted(p: PersistedQueueItem): DownloadItem {
return {
...p,
// A download in flight when we quit lost its process; re-queue it (yt-dlp
// continues the .part on relaunch). Progress restarts from 0.
status: p.status === 'downloading' ? 'queued' : p.status,
progress: 0
}
}
/** The persistable subset of the live queue, in durable form. */
export function persistableItems(items: DownloadItem[]): PersistedQueueItem[] {
return items
.filter((i) => (PERSISTED_STATUSES as readonly string[]).includes(i.status))
.map(toPersisted)
}