From 10b5d9585239f1193233b032f97967dce487c996 Mon Sep 17 00:00:00 2001 From: vicrodh <103399283+vicrodh@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:57:57 -0600 Subject: [PATCH] feat(updater): add performAutoUpdate and eligibility to store/service Additive changes only: adds isAutoUpdateEligible() getter and eligibility fetch in initUpdatesStore (keeps existing skipIfRemote guards); adds performAutoUpdate() in updatesService for the download/install/relaunch flow with cancellation support. cc --- src/lib/services/updatesService.ts | 76 ++++++++++++++++++++++++++++++ src/lib/stores/updatesStore.ts | 17 +++++++ 2 files changed, 93 insertions(+) diff --git a/src/lib/services/updatesService.ts b/src/lib/services/updatesService.ts index d42fc1545..4f6797d83 100644 --- a/src/lib/services/updatesService.ts +++ b/src/lib/services/updatesService.ts @@ -1,5 +1,7 @@ import { invoke } from '@tauri-apps/api/core'; import { openUrl } from '@tauri-apps/plugin-opener'; +import { check } from '@tauri-apps/plugin-updater'; +import { relaunch } from '@tauri-apps/plugin-process'; import { acknowledgeRelease, checkForUpdates, @@ -316,3 +318,77 @@ export async function disableUpdateChecks(): Promise { console.debug('[Updates] Failed to disable update checks:', error); } } + +// ==================== Auto-update ==================== + +export type AutoUpdateProgress = + | { state: 'checking' } + | { state: 'downloading'; downloadedBytes: number; totalBytes?: number } + | { state: 'installing' } + | { state: 'restarting' } + | { state: 'error'; error: string }; + +let autoUpdateInProgress = false; + +export function isAutoUpdateInProgress(): boolean { + return autoUpdateInProgress; +} + +/** + * Download, install and relaunch the pending update. + * + * `isCancelled` is polled before kicking off the download and again before the + * final relaunch so the caller can abort mid-flight. + * Returns true when the relaunch was triggered, false otherwise. + */ +export async function performAutoUpdate( + onProgress: (progress: AutoUpdateProgress) => void, + isCancelled?: () => boolean, +): Promise { + if (autoUpdateInProgress) { + console.debug('[Updates] Auto-update already in progress'); + return false; + } + autoUpdateInProgress = true; + try { + onProgress({ state: 'checking' }); + const update = await check(); + if (!update) return false; + + if (isCancelled?.()) return false; + + let totalBytes: number | undefined; + let downloadedBytes = 0; + + onProgress({ state: 'downloading', downloadedBytes: 0 }); + + await update.downloadAndInstall((event) => { + switch (event.event) { + case 'Started': + totalBytes = event.data.contentLength ?? undefined; + onProgress({ state: 'downloading', downloadedBytes: 0, totalBytes }); + break; + case 'Progress': + downloadedBytes += event.data.chunkLength; + onProgress({ state: 'downloading', downloadedBytes, totalBytes }); + break; + case 'Finished': + onProgress({ state: 'installing' }); + break; + } + }); + + if (isCancelled?.()) return false; + + onProgress({ state: 'restarting' }); + await relaunch(); + return true; + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + console.error('[Updates] Auto-update failed:', message); + onProgress({ state: 'error', error: message }); + return false; + } finally { + autoUpdateInProgress = false; + } +} diff --git a/src/lib/stores/updatesStore.ts b/src/lib/stores/updatesStore.ts index e609c80d3..dd7014919 100644 --- a/src/lib/stores/updatesStore.ts +++ b/src/lib/stores/updatesStore.ts @@ -31,8 +31,10 @@ let preferences: UpdatePreferences = { }; let currentVersion = ''; +let autoUpdateEligible = false; let versionLoaded = false; let prefsLoaded = false; +let eligibilityLoaded = false; const listeners = new Set<() => void>(); @@ -63,6 +65,10 @@ export function getCurrentVersion(): string { return currentVersion; } +export function isAutoUpdateEligible(): boolean { + return autoUpdateEligible; +} + export async function initUpdatesStore(): Promise { if (skipIfRemote()) return; // get_current_version never needs a session (compile-time value). @@ -76,6 +82,17 @@ export async function initUpdatesStore(): Promise { } } + // Auto-update eligibility is compile-time + runtime (APPIMAGE env, flatpak/snap + // detection). No session required. Only fetch once. + if (!eligibilityLoaded) { + try { + autoUpdateEligible = await invoke('v2_is_auto_update_eligible'); + eligibilityLoaded = true; + } catch (error) { + console.debug('[Updates] Failed to check auto-update eligibility:', error); + } + } + // get_update_preferences needs the SQLite store which is only // available after login. Retry on each call until it succeeds, // so a pre-login failure doesn't lock in stale defaults.