Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions desktop/src/main/__tests__/updater.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,65 @@ describe("updater", () => {
electronUpdaterMock.autoUpdater.emit("update-downloaded", { version: "9.9.9" })
expect((electron.dialog.showMessageBox as ReturnType<typeof vi.fn>)).not.toHaveBeenCalled()
})

it("checks at boot and again on the recheck interval", async () => {
vi.useFakeTimers()
try {
const { initAutoUpdater, stopAutoUpdater } = await import("../updater.js")
const send = vi.fn()
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)

expect(electronUpdaterMock.autoUpdater.checkForUpdates).not.toHaveBeenCalled()
await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)

await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(2)

stopAutoUpdater()
await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(2)
} finally {
vi.useRealTimers()
}
})

it("clears the pending boot check when stopped before it fires", async () => {
vi.useFakeTimers()
try {
const { initAutoUpdater, stopAutoUpdater } = await import("../updater.js")
const send = vi.fn()
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)

// Quit before the 10s boot delay elapses.
stopAutoUpdater()
await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).not.toHaveBeenCalled()
} finally {
vi.useRealTimers()
}
})

it("skips the background check while a download is in flight", async () => {
vi.useFakeTimers()
try {
const { initAutoUpdater, stopAutoUpdater } = await import("../updater.js")
const send = vi.fn()
const win = { isDestroyed: () => false, webContents: { send } } as never
await initAutoUpdater(() => win)

await vi.advanceTimersByTimeAsync(10_000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)

electronUpdaterMock.autoUpdater.emit("update-downloaded", { version: "9.9.9" })
await vi.advanceTimersByTimeAsync(6 * 60 * 60 * 1000)
expect(electronUpdaterMock.autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1)

stopAutoUpdater()
} finally {
vi.useRealTimers()
}
})
})
3 changes: 2 additions & 1 deletion desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { LogStore } from "./log-store.js"
import { PtyManager } from "./pty.js"
import { DaemonState } from "./state.js"
import { AppTray } from "./tray.js"
import { initAutoUpdater } from "./updater.js"
import { initAutoUpdater, stopAutoUpdater } from "./updater.js"
import { Watcher } from "./watcher.js"

const PROTOCOL = "devsy"
Expand Down Expand Up @@ -157,6 +157,7 @@ app.whenReady().then(() => {
}
daemonManager.stop()
ptyManager.destroyAll()
stopAutoUpdater()
})

// Register IPC handlers
Expand Down
32 changes: 30 additions & 2 deletions desktop/src/main/updater.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,18 @@ function saveSettings(patch: PersistedSettings): void {
}
}

// Delay before the first check so it doesn't compete with app startup, then
// re-check on a fixed interval so long-running sessions still discover releases
// published after launch.
const INITIAL_CHECK_DELAY_MS = 10_000
const RECHECK_INTERVAL_MS = 6 * 60 * 60 * 1000

let currentChannel: ReleaseChannel = "stable"
let autoDownloadEnabled = true
let getMainWindowFn: (() => BrowserWindow | null) | null = null
let lastStatus: UpdateStatus = { state: "idle" }
let initialCheckTimer: ReturnType<typeof setTimeout> | null = null
let recheckTimer: ReturnType<typeof setInterval> | null = null

function sendUpdateStatus(status: UpdateStatus): void {
const win = getMainWindowFn?.()
Expand Down Expand Up @@ -246,11 +254,31 @@ export async function initAutoUpdater(
console.error("Auto-update error:", err.message)
})

setTimeout(() => {
const runBackgroundCheck = (): void => {
// Skip while a download is already in flight or staged — re-fetching the
// manifest would just churn state. autoUpdater.autoDownload is already set
// from the user's preference, so a plain check honors that toggle.
if (lastStatus.state === "downloading" || lastStatus.state === "downloaded") {
return
}
autoUpdater.checkForUpdates().catch((err: Error) => {
console.error("Update check failed:", err.message)
})
}, 10_000)
}

initialCheckTimer = setTimeout(runBackgroundCheck, INITIAL_CHECK_DELAY_MS)
recheckTimer = setInterval(runBackgroundCheck, RECHECK_INTERVAL_MS)
}

export function stopAutoUpdater(): void {
if (initialCheckTimer) {
clearTimeout(initialCheckTimer)
initialCheckTimer = null
}
if (recheckTimer) {
clearInterval(recheckTimer)
recheckTimer = null
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async function getUpdater() {
Expand Down
16 changes: 3 additions & 13 deletions desktop/src/renderer/src/lib/components/update/UpdateDialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
lastCheckedAt,
} from "$lib/stores/updates.svelte.js"
import { markUserInitiated } from "./update-toasts.js"
import { fmtMBps, fmtTime } from "./status-copy.js"

let {
open = $bindable(false),
Expand All @@ -26,17 +27,6 @@ const sanitizedNotes = $derived(
s.releaseNotes ? DOMPurify.sanitize(s.releaseNotes) : "",
)

function fmtMBps(bps: number): string {
if (!bps) return ""
const mbps = bps / 1_000_000
return `${mbps.toFixed(2)} MB/s`
}

function fmtTime(ts: number | null): string {
if (!ts) return ""
return new Date(ts).toLocaleTimeString()
}

async function onCheck() {
markUserInitiated()
await checkForUpdates()
Expand Down Expand Up @@ -80,7 +70,7 @@ async function onInstall() {
<p class="text-sm font-medium">Downloading v{s.version}…</p>
<Progress value={s.progress?.percent ?? 0} max={100} />
<p class="text-xs text-muted-foreground">
{(s.progress?.percent ?? 0).toFixed(0)}% · {fmtMBps(s.progress?.bytesPerSecond ?? 0)}
{(s.progress?.percent ?? 0).toFixed(0)}% · {fmtMBps(s.progress?.bytesPerSecond)}
</p>
</div>
{:else if s.state === "downloaded"}
Expand All @@ -95,7 +85,7 @@ async function onInstall() {
{/if}
<div class="flex gap-2 justify-end">
<Button variant="ghost" onclick={() => (open = false)}>Later</Button>
<Button onclick={onInstall}>Restart and Update</Button>
<Button onclick={onInstall}>Restart &amp; Update</Button>
</div>
</div>
{:else if s.state === "not-available"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe("UpdateDialog", () => {
__setForTest({ state: "downloaded", version: "9.9.9" })
render(UpdateDialog, { props: { open: true } })
expect(bodyText()).toMatch(/version 9\.9\.9/i)
expect(queryButton(/restart and update/i)).toBeTruthy()
expect(queryButton(/restart & update/i)).toBeTruthy()
})

it("renders 'downloading' progress", () => {
Expand Down
Loading
Loading