feat(audit): Batch 15 — download-engine lifecycle (safe part; rest live-verify)
This is the batch the run reserves for a WATCHED LIVE SESSION, so only the safe,
non-file-deleting item is implemented here; the rest are staged with per-item
live-verify rationale in CODE-AUDIT.md.
Landed:
- L157: removeSource cancels its in-flight downloads. removeSource emits a
`sourceRemoved` event on the coordinator bus (C2); the downloads store cancels
every active (downloading/queued) item whose mediaItemId starts with
`${sourceId}:` — MediaItem ids are `${sourceId}:${videoId}` (indexerCore), so the
prefix reliably identifies the source's downloads. Reuses the proven cancel()
path (process kill + pump). Type/lint/test-green; process-kill worth a live confirm.
Resolved by prior work:
- UX11: M4 persists the queue, so a scheduled item fires on next launch once due —
no longer "never fires if quit".
- UX20: L68 resets notifiedBackground on window show, so every close-to-tray
re-fires the "still running in the tray" notice, not just the first.
Deferred to the watched live pass (each needs a real download to verify):
R4 (delete .part on cancel — file-deletion data-safety), L65 (graceful Ctrl-C
pause), L139 (spawn↔auto-update interlock — timing race), B2 (index AbortSignal +
Cancel), B6 (app-update cancel — ties to CL4's deferred updater), L143 (in-window
quit-anyway).
Verified: typecheck (node+web) + 279 tests + eslint + production build green;
touched files prettier-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -13,12 +13,15 @@ import type { AddEntry } from './downloads'
|
||||
*
|
||||
* sources --enqueueDownloads--> downloads (queue a channel's media items)
|
||||
* downloads --downloadCompleted--> sources (mark the source item downloaded)
|
||||
* sources --sourceRemoved------> downloads (cancel that source's in-flight downloads)
|
||||
*/
|
||||
export interface StoreEvents {
|
||||
/** sources → downloads: enqueue a batch of media items as downloads */
|
||||
enqueueDownloads: AddEntry[]
|
||||
/** downloads → sources: a download tied to a source MediaItem finished */
|
||||
downloadCompleted: { mediaItemId: string; filePath?: string }
|
||||
/** sources → downloads: a source was removed; cancel its in-flight downloads (L157) */
|
||||
sourceRemoved: string
|
||||
}
|
||||
|
||||
type Handler<K extends keyof StoreEvents> = (payload: StoreEvents[K]) => void
|
||||
|
||||
@@ -462,6 +462,23 @@ export const useDownloads = create<DownloadState>((set, get) => {
|
||||
// no direct import from sources, so the two stores stay acyclic.
|
||||
on('enqueueDownloads', (entries) => useDownloads.getState().addMany(entries))
|
||||
|
||||
// A removed source cancels its in-flight downloads (L157): MediaItem ids are
|
||||
// `${sourceId}:${videoId}` (indexerCore), so a download's mediaItemId prefix
|
||||
// identifies its source. Only active (downloading/queued) items are canceled;
|
||||
// finished/failed ones are left alone. cancel() owns the process kill + pump.
|
||||
on('sourceRemoved', (sourceId) => {
|
||||
const st = useDownloads.getState()
|
||||
const prefix = `${sourceId}:`
|
||||
for (const it of st.items) {
|
||||
if (
|
||||
(it.status === 'downloading' || it.status === 'queued') &&
|
||||
it.mediaItemId?.startsWith(prefix)
|
||||
) {
|
||||
st.cancel(it.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// M4: mirror the durable queue to main whenever it changes, so saved/scheduled and
|
||||
// other pending items survive a quit. Skipped in preview (no IPC bridge). A
|
||||
// progress-only update doesn't churn this — toPersisted drops progress/speed/eta, so
|
||||
|
||||
@@ -178,6 +178,10 @@ export const useSources = create<SourcesState>((set, get) => ({
|
||||
sources: s.sources.filter((x) => x.id !== id),
|
||||
selectedSourceId: s.selectedSourceId === id ? null : s.selectedSourceId
|
||||
}))
|
||||
// Cancel any of this source's downloads still in flight, so removing a source
|
||||
// stops its videos from finishing into now-orphaned folders (L157). Routed via
|
||||
// the event bus (C2) so sources doesn't import downloads.
|
||||
emit('sourceRemoved', id)
|
||||
if (!PREVIEW) window.api.removeSource(id).catch(logError('removeSource'))
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user