Fix L29/L52: QUALITY_OPTIONS moved to UI module; installer spawn via child_process

L29: QUALITY_OPTIONS was a presentational constant living in store/downloads.ts,
     coupling views to the store. Moved to src/renderer/src/qualityOptions.ts,
     which assembles it directly from @shared/ipc. store/downloads.ts no longer
     imports VIDEO/AUDIO_QUALITY_OPTIONS. Both consumers (DownloadBar, SettingsView)
     updated to import from the new module.

L52: Replaced the fixed `setTimeout(app.quit, 1500)` installer handoff with a
     detached spawn. The installer is spawned with stdio:'ignore' and unref'd so
     it outlives AeroFetch even on a slow machine. app.quit() fires on setImmediate
     so the IPC response returns to the renderer before the process exits.

typecheck + 242 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:32:50 -04:00
parent 1f2465576b
commit a4fd0feb6b
6 changed files with 21 additions and 21 deletions
+9 -8
View File
@@ -1,4 +1,5 @@
import { app, net, shell, type WebContents } from 'electron'
import { app, net, type WebContents } from 'electron'
import { spawn } from 'child_process'
import { createWriteStream, type WriteStream } from 'fs'
import { stat, unlink } from 'fs/promises'
import { join, normalize, dirname } from 'path'
@@ -413,9 +414,6 @@ export async function downloadAppUpdate(url: string, wc: WebContents): Promise<A
})
}
/** Let the launched installer spawn before we quit to release our files (ms). */
const INSTALLER_HANDOFF_MS = 1500
/** Launch a freshly-downloaded installer, then quit so it can replace the app. */
export async function runAppUpdate(filePath: string): Promise<{ ok: boolean; error?: string }> {
try {
@@ -428,10 +426,13 @@ export async function runAppUpdate(filePath: string): Promise<{ ok: boolean; err
return { ok: false, error: 'Refused to run an unexpected file.' }
}
await stat(target) // throws if the file is missing
const err = await shell.openPath(target) // hand the installer to the OS
if (err) return { ok: false, error: err }
// Give the installer a beat to spawn, then quit so it can overwrite our files.
setTimeout(() => app.quit(), INSTALLER_HANDOFF_MS)
// Spawn the installer detached + unref'd so it outlives our process even on a
// slow machine. shell.openPath goes through ShellExecute but gives us no child
// PID to wait on; spawning directly lets us call unref() before we quit.
const child = spawn(target, [], { detached: true, stdio: 'ignore', shell: false })
child.unref()
// Let the IPC response reach the renderer before we exit.
setImmediate(() => app.quit())
return { ok: true }
} catch (e) {
return { ok: false, error: e instanceof Error ? e.message : String(e) }
+2 -1
View File
@@ -28,7 +28,8 @@ import {
WarningRegular
} from '@fluentui/react-icons'
import type { MediaInfo, FormatOption, PlaylistInfo } from '@shared/ipc'
import { useDownloads, QUALITY_OPTIONS, type MediaKind } from '../store/downloads'
import { useDownloads, type MediaKind } from '../store/downloads'
import { QUALITY_OPTIONS } from '../qualityOptions'
import { sameVideo } from '../store/queueStats'
import { useSettings } from '../store/settings'
import { Select } from './Select'
+2 -1
View File
@@ -55,7 +55,8 @@ import {
} from '@shared/ipc'
import { useSettings } from '../store/settings'
import { useSystemTheme } from '../store/systemTheme'
import { QUALITY_OPTIONS, useDownloads } from '../store/downloads'
import { useDownloads } from '../store/downloads'
import { QUALITY_OPTIONS } from '../qualityOptions'
import { useTemplates } from '../store/templates'
import { useErrorLog } from '../store/errorlog'
import { Select } from './Select'
+6
View File
@@ -0,0 +1,6 @@
import { VIDEO_QUALITY_OPTIONS, AUDIO_QUALITY_OPTIONS } from '@shared/ipc'
export const QUALITY_OPTIONS = {
video: VIDEO_QUALITY_OPTIONS,
audio: AUDIO_QUALITY_OPTIONS
} as const
-9
View File
@@ -1,7 +1,5 @@
import { create } from 'zustand'
import {
VIDEO_QUALITY_OPTIONS,
AUDIO_QUALITY_OPTIONS,
type DownloadEvent,
type DownloadMeta,
type DownloadOptions,
@@ -71,13 +69,6 @@ export interface DownloadItem {
mediaItemId?: string
}
// `satisfies` (not an annotation) so the tuple element types survive: under
// noUncheckedIndexedAccess a `readonly string[]` index yields `string | undefined`,
// but the const tuples keep `QUALITY_OPTIONS[kind][0]` known-defined (L168).
export const QUALITY_OPTIONS = {
video: VIDEO_QUALITY_OPTIONS,
audio: AUDIO_QUALITY_OPTIONS
} satisfies Record<MediaKind, readonly string[]>
// Placeholder channel shown while metadata is still being fetched. Tracked as a
// constant so the meta/error/done handlers can recognise and clear it instead of